001 /*
002 * Copyright (C) 2009 Google Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017 package com.google.common.util.concurrent;
018
019 import static com.google.common.base.Preconditions.checkNotNull;
020 import static com.google.common.base.Preconditions.checkState;
021
022 import com.google.common.annotations.Beta;
023 import com.google.common.base.Service;
024 import com.google.common.base.Service.State; // javadoc needs this
025 import com.google.common.base.Throwables;
026
027 import java.util.concurrent.CountDownLatch;
028 import java.util.concurrent.ExecutionException;
029 import java.util.concurrent.Future;
030 import java.util.concurrent.TimeUnit;
031 import java.util.concurrent.TimeoutException;
032 import java.util.concurrent.locks.ReentrantLock;
033
034 /**
035 * Base class for implementing services that can handle {@link #doStart} and
036 * {@link #doStop} requests, responding to them with {@link #notifyStarted()}
037 * and {@link #notifyStopped()} callbacks. Its subclasses must manage threads
038 * manually; consider {@link AbstractExecutionThreadService} if you need only a
039 * single execution thread.
040 *
041 * @author Jesse Wilson
042 * @since 1
043 */
044 @Beta
045 public abstract class AbstractService implements Service {
046
047 private final ReentrantLock lock = new ReentrantLock();
048
049 private final Transition startup = new Transition();
050 private final Transition shutdown = new Transition();
051
052 /**
053 * The internal state, which equals external state unless
054 * shutdownWhenStartupFinishes is true. Guarded by {@code lock}.
055 */
056 private State state = State.NEW;
057
058 /**
059 * If true, the user requested a shutdown while the service was still starting
060 * up. Guarded by {@code lock}.
061 */
062 private boolean shutdownWhenStartupFinishes = false;
063
064 /**
065 * This method is called by {@link #start} to initiate service startup. The
066 * invocation of this method should cause a call to {@link #notifyStarted()},
067 * either during this method's run, or after it has returned. If startup
068 * fails, the invocation should cause a call to {@link
069 * #notifyFailed(Throwable)} instead.
070 *
071 * <p>This method should return promptly; prefer to do work on a different
072 * thread where it is convenient. It is invoked exactly once on service
073 * startup, even when {@link #start} is called multiple times.
074 */
075 protected abstract void doStart();
076
077 /**
078 * This method should be used to initiate service shutdown. The invocation
079 * of this method should cause a call to {@link #notifyStopped()}, either
080 * during this method's run, or after it has returned. If shutdown fails, the
081 * invocation should cause a call to {@link #notifyFailed(Throwable)} instead.
082 *
083 * <p>This method should return promptly; prefer to do work on a different
084 * thread where it is convenient. It is invoked exactly once on service
085 * shutdown, even when {@link #stop} is called multiple times.
086 */
087 protected abstract void doStop();
088
089 public final Future<State> start() {
090 lock.lock();
091 try {
092 if (state == State.NEW) {
093 state = State.STARTING;
094 doStart();
095 }
096 } catch (Throwable startupFailure) {
097 // put the exception in the future, the user can get it via Future.get()
098 notifyFailed(startupFailure);
099 } finally {
100 lock.unlock();
101 }
102
103 return startup;
104 }
105
106 public final Future<State> stop() {
107 lock.lock();
108 try {
109 if (state == State.NEW) {
110 state = State.TERMINATED;
111 startup.transitionSucceeded(State.TERMINATED);
112 shutdown.transitionSucceeded(State.TERMINATED);
113 } else if (state == State.STARTING) {
114 shutdownWhenStartupFinishes = true;
115 startup.transitionSucceeded(State.STOPPING);
116 } else if (state == State.RUNNING) {
117 state = State.STOPPING;
118 doStop();
119 }
120 } catch (Throwable shutdownFailure) {
121 // put the exception in the future, the user can get it via Future.get()
122 notifyFailed(shutdownFailure);
123 } finally {
124 lock.unlock();
125 }
126
127 return shutdown;
128 }
129
130 public State startAndWait() {
131 try {
132 return start().get();
133 } catch (InterruptedException e) {
134 Thread.currentThread().interrupt();
135 throw new RuntimeException(e);
136 } catch (ExecutionException e) {
137 throw Throwables.propagate(e.getCause());
138 }
139 }
140
141 public State stopAndWait() {
142 try {
143 return stop().get();
144 } catch (ExecutionException e) {
145 throw Throwables.propagate(e.getCause());
146 } catch (InterruptedException e) {
147 Thread.currentThread().interrupt();
148 throw new RuntimeException(e);
149 }
150 }
151
152 /**
153 * Implementing classes should invoke this method once their service has
154 * started. It will cause the service to transition from {@link
155 * State#STARTING} to {@link State#RUNNING}.
156 *
157 * @throws IllegalStateException if the service is not
158 * {@link State#STARTING}.
159 */
160 protected final void notifyStarted() {
161 lock.lock();
162 try {
163 if (state != State.STARTING) {
164 IllegalStateException failure = new IllegalStateException(
165 "Cannot notifyStarted() when the service is " + state);
166 notifyFailed(failure);
167 throw failure;
168 }
169
170 state = State.RUNNING;
171 if (shutdownWhenStartupFinishes) {
172 stop();
173 } else {
174 startup.transitionSucceeded(State.RUNNING);
175 }
176 } finally {
177 lock.unlock();
178 }
179 }
180
181 /**
182 * Implementing classes should invoke this method once their service has
183 * stopped. It will cause the service to transition from {@link
184 * State#STOPPING} to {@link State#TERMINATED}.
185 *
186 * @throws IllegalStateException if the service is neither {@link
187 * State#STOPPING} nor {@link State#RUNNING}.
188 */
189 protected final void notifyStopped() {
190 lock.lock();
191 try {
192 if (state != State.STOPPING && state != State.RUNNING) {
193 IllegalStateException failure = new IllegalStateException(
194 "Cannot notifyStopped() when the service is " + state);
195 notifyFailed(failure);
196 throw failure;
197 }
198
199 state = State.TERMINATED;
200 shutdown.transitionSucceeded(State.TERMINATED);
201 } finally {
202 lock.unlock();
203 }
204 }
205
206 /**
207 * Invoke this method to transition the service to the
208 * {@link State#FAILED}. The service will <b>not be stopped</b> if it
209 * is running. Invoke this method when a service has failed critically or
210 * otherwise cannot be started nor stopped.
211 */
212 protected final void notifyFailed(Throwable cause) {
213 checkNotNull(cause);
214
215 lock.lock();
216 try {
217 if (state == State.STARTING) {
218 startup.transitionFailed(cause);
219 shutdown.transitionFailed(new Exception(
220 "Service failed to start.", cause));
221 } else if (state == State.STOPPING) {
222 shutdown.transitionFailed(cause);
223 }
224
225 state = State.FAILED;
226 } finally {
227 lock.unlock();
228 }
229 }
230
231 public final boolean isRunning() {
232 return state() == State.RUNNING;
233 }
234
235 public final State state() {
236 lock.lock();
237 try {
238 if (shutdownWhenStartupFinishes && state == State.STARTING) {
239 return State.STOPPING;
240 } else {
241 return state;
242 }
243 } finally {
244 lock.unlock();
245 }
246 }
247
248 @Override public String toString() {
249 return getClass().getSimpleName() + " [" + state() + "]";
250 }
251
252 /**
253 * A change from one service state to another, plus the result of the change.
254 *
255 * TODO: could this be renamed to DefaultFuture, with methods
256 * like setResult(T) and setFailure(T) ?
257 */
258 private class Transition implements Future<State> {
259 private final CountDownLatch done = new CountDownLatch(1);
260 private State result;
261 private Throwable failureCause;
262
263 void transitionSucceeded(State result) {
264 // guarded by AbstractService.lock
265 checkState(this.result == null);
266 this.result = result;
267 done.countDown();
268 }
269
270 void transitionFailed(Throwable cause) {
271 // guarded by AbstractService.lock
272 checkState(result == null);
273 this.result = State.FAILED;
274 this.failureCause = cause;
275 done.countDown();
276 }
277
278 public boolean cancel(boolean mayInterruptIfRunning) {
279 return false;
280 }
281
282 public boolean isCancelled() {
283 return false;
284 }
285
286 public boolean isDone() {
287 return done.getCount() == 0;
288 }
289
290 public State get() throws InterruptedException, ExecutionException {
291 done.await();
292 return getImmediately();
293 }
294
295 public State get(long timeout, TimeUnit unit)
296 throws InterruptedException, ExecutionException, TimeoutException {
297 if (done.await(timeout, unit)) {
298 return getImmediately();
299 }
300 throw new TimeoutException(AbstractService.this.toString());
301 }
302
303 private State getImmediately() throws ExecutionException {
304 if (result == State.FAILED) {
305 throw new ExecutionException(failureCause);
306 } else {
307 return result;
308 }
309 }
310 }
311 }