001 /*
002 * Copyright (C) 2009 The Guava Authors
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.checkArgument;
020 import static com.google.common.base.Preconditions.checkNotNull;
021 import static com.google.common.base.Preconditions.checkState;
022
023 import com.google.common.annotations.Beta;
024 import com.google.common.collect.Lists;
025 import com.google.common.collect.Queues;
026 import com.google.common.util.concurrent.Service.State; // javadoc needs this
027
028 import java.util.List;
029 import java.util.Queue;
030 import java.util.concurrent.ExecutionException;
031 import java.util.concurrent.Executor;
032 import java.util.concurrent.TimeUnit;
033 import java.util.concurrent.TimeoutException;
034 import java.util.concurrent.locks.ReentrantLock;
035 import java.util.logging.Level;
036 import java.util.logging.Logger;
037
038 import javax.annotation.Nullable;
039 import javax.annotation.concurrent.GuardedBy;
040 import javax.annotation.concurrent.Immutable;
041
042 /**
043 * Base class for implementing services that can handle {@link #doStart} and {@link #doStop}
044 * requests, responding to them with {@link #notifyStarted()} and {@link #notifyStopped()}
045 * callbacks. Its subclasses must manage threads manually; consider
046 * {@link AbstractExecutionThreadService} if you need only a single execution thread.
047 *
048 * @author Jesse Wilson
049 * @author Luke Sandberg
050 * @since 1.0
051 */
052 @Beta
053 public abstract class AbstractService implements Service {
054 private static final Logger logger = Logger.getLogger(AbstractService.class.getName());
055 private final ReentrantLock lock = new ReentrantLock();
056
057 private final Transition startup = new Transition();
058 private final Transition shutdown = new Transition();
059
060 /**
061 * The listeners to notify during a state transition.
062 */
063 @GuardedBy("lock")
064 private final List<ListenerExecutorPair> listeners = Lists.newArrayList();
065
066 /**
067 * The queue of listeners that are waiting to be executed.
068 *
069 * <p>Enqueue operations should be protected by {@link #lock} while dequeue operations should be
070 * protected by the implicit lock on this object. Dequeue operations should be executed atomically
071 * with the execution of the {@link Runnable} and additionally the {@link #lock} should not be
072 * held when the listeners are being executed. Use {@link #executeListeners} for this operation.
073 * This is necessary to ensure that elements on the queue are executed in the correct order.
074 * Enqueue operations should be protected so that listeners are added in the correct order. We use
075 * a concurrent queue implementation so that enqueues can be executed concurrently with dequeues.
076 */
077 @GuardedBy("queuedListeners")
078 private final Queue<Runnable> queuedListeners = Queues.newConcurrentLinkedQueue();
079
080 /**
081 * The current state of the service. This should be written with the lock held but can be read
082 * without it because it is an immutable object in a volatile field. This is desirable so that
083 * methods like {@link #state}, {@link #failureCause} and notably {@link #toString} can be run
084 * without grabbing the lock.
085 *
086 * <p>To update this field correctly the lock must be held to guarantee that the state is
087 * consistent.
088 */
089 @GuardedBy("lock")
090 private volatile StateSnapshot snapshot = new StateSnapshot(State.NEW);
091
092 protected AbstractService() {
093 // Add a listener to update the futures. This needs to be added first so that it is executed
094 // before the other listeners. This way the other listeners can access the completed futures.
095 addListener(
096 new Listener() {
097 public void starting() {}
098
099 public void running() {
100 startup.set(State.RUNNING);
101 }
102
103 public void stopping(State from) {
104 if (from == State.STARTING) {
105 startup.set(State.STOPPING);
106 }
107 }
108
109 public void terminated(State from) {
110 if (from == State.NEW) {
111 startup.set(State.TERMINATED);
112 }
113 shutdown.set(State.TERMINATED);
114 }
115
116 public void failed(State from, Throwable failure) {
117 switch (from) {
118 case STARTING:
119 startup.setException(failure);
120 shutdown.setException(new Exception("Service failed to start.", failure));
121 break;
122 case RUNNING:
123 shutdown.setException(new Exception("Service failed while running", failure));
124 break;
125 case STOPPING:
126 shutdown.setException(failure);
127 break;
128 case TERMINATED: /* fall-through */
129 case FAILED: /* fall-through */
130 case NEW: /* fall-through */
131 default:
132 throw new AssertionError("Unexpected from state: " + from);
133 }
134 }
135 },
136 MoreExecutors.sameThreadExecutor());
137 }
138
139 /**
140 * This method is called by {@link #start} to initiate service startup. The invocation of this
141 * method should cause a call to {@link #notifyStarted()}, either during this method's run, or
142 * after it has returned. If startup fails, the invocation should cause a call to
143 * {@link #notifyFailed(Throwable)} instead.
144 *
145 * <p>This method should return promptly; prefer to do work on a different thread where it is
146 * convenient. It is invoked exactly once on service startup, even when {@link #start} is called
147 * multiple times.
148 */
149 protected abstract void doStart();
150
151 /**
152 * This method should be used to initiate service shutdown. The invocation of this method should
153 * cause a call to {@link #notifyStopped()}, either during this method's run, or after it has
154 * returned. If shutdown fails, the invocation should cause a call to
155 * {@link #notifyFailed(Throwable)} instead.
156 *
157 * <p> This method should return promptly; prefer to do work on a different thread where it is
158 * convenient. It is invoked exactly once on service shutdown, even when {@link #stop} is called
159 * multiple times.
160 */
161 protected abstract void doStop();
162
163 public final ListenableFuture<State> start() {
164 lock.lock();
165 try {
166 if (snapshot.state == State.NEW) {
167 snapshot = new StateSnapshot(State.STARTING);
168 starting();
169 doStart();
170 }
171 } catch (Throwable startupFailure) {
172 notifyFailed(startupFailure);
173 } finally {
174 lock.unlock();
175 executeListeners();
176 }
177
178 return startup;
179 }
180
181 public final ListenableFuture<State> stop() {
182 lock.lock();
183 try {
184 switch (snapshot.state) {
185 case NEW:
186 snapshot = new StateSnapshot(State.TERMINATED);
187 terminated(State.NEW);
188 break;
189 case STARTING:
190 snapshot = new StateSnapshot(State.STARTING, true, null);
191 stopping(State.STARTING);
192 break;
193 case RUNNING:
194 snapshot = new StateSnapshot(State.STOPPING);
195 stopping(State.RUNNING);
196 doStop();
197 break;
198 case STOPPING:
199 case TERMINATED:
200 case FAILED:
201 // do nothing
202 break;
203 default:
204 throw new AssertionError("Unexpected state: " + snapshot.state);
205 }
206 } catch (Throwable shutdownFailure) {
207 notifyFailed(shutdownFailure);
208 } finally {
209 lock.unlock();
210 executeListeners();
211 }
212
213 return shutdown;
214 }
215
216 public State startAndWait() {
217 return Futures.getUnchecked(start());
218 }
219
220 public State stopAndWait() {
221 return Futures.getUnchecked(stop());
222 }
223
224 /**
225 * Implementing classes should invoke this method once their service has started. It will cause
226 * the service to transition from {@link State#STARTING} to {@link State#RUNNING}.
227 *
228 * @throws IllegalStateException if the service is not {@link State#STARTING}.
229 */
230 protected final void notifyStarted() {
231 lock.lock();
232 try {
233 if (snapshot.state != State.STARTING) {
234 IllegalStateException failure = new IllegalStateException(
235 "Cannot notifyStarted() when the service is " + snapshot.state);
236 notifyFailed(failure);
237 throw failure;
238 }
239
240 if (snapshot.shutdownWhenStartupFinishes) {
241 snapshot = new StateSnapshot(State.STOPPING);
242 // We don't call listeners here because we already did that when we set the
243 // shutdownWhenStartupFinishes flag.
244 doStop();
245 } else {
246 snapshot = new StateSnapshot(State.RUNNING);
247 running();
248 }
249 } finally {
250 lock.unlock();
251 executeListeners();
252 }
253 }
254
255 /**
256 * Implementing classes should invoke this method once their service has stopped. It will cause
257 * the service to transition from {@link State#STOPPING} to {@link State#TERMINATED}.
258 *
259 * @throws IllegalStateException if the service is neither {@link State#STOPPING} nor
260 * {@link State#RUNNING}.
261 */
262 protected final void notifyStopped() {
263 lock.lock();
264 try {
265 if (snapshot.state != State.STOPPING && snapshot.state != State.RUNNING) {
266 IllegalStateException failure = new IllegalStateException(
267 "Cannot notifyStopped() when the service is " + snapshot.state);
268 notifyFailed(failure);
269 throw failure;
270 }
271 State previous = snapshot.state;
272 snapshot = new StateSnapshot(State.TERMINATED);
273 terminated(previous);
274 } finally {
275 lock.unlock();
276 executeListeners();
277 }
278 }
279
280 /**
281 * Invoke this method to transition the service to the {@link State#FAILED}. The service will
282 * <b>not be stopped</b> if it is running. Invoke this method when a service has failed critically
283 * or otherwise cannot be started nor stopped.
284 */
285 protected final void notifyFailed(Throwable cause) {
286 checkNotNull(cause);
287
288 lock.lock();
289 try {
290 switch (snapshot.state) {
291 case NEW:
292 case TERMINATED:
293 throw new IllegalStateException("Failed while in state:" + snapshot.state, cause);
294 case RUNNING:
295 case STARTING:
296 case STOPPING:
297 State previous = snapshot.state;
298 snapshot = new StateSnapshot(State.FAILED, false, cause);
299 failed(previous, cause);
300 break;
301 case FAILED:
302 // Do nothing
303 break;
304 default:
305 throw new AssertionError("Unexpected state: " + snapshot.state);
306 }
307 } finally {
308 lock.unlock();
309 executeListeners();
310 }
311 }
312
313 public final boolean isRunning() {
314 return state() == State.RUNNING;
315 }
316
317 public final State state() {
318 return snapshot.externalState();
319 }
320
321
322 public final void addListener(Listener listener, Executor executor) {
323 checkNotNull(listener, "listener");
324 checkNotNull(executor, "executor");
325 lock.lock();
326 try {
327 if (snapshot.state != State.TERMINATED && snapshot.state != State.FAILED) {
328 listeners.add(new ListenerExecutorPair(listener, executor));
329 }
330 } finally {
331 lock.unlock();
332 }
333 }
334
335
336 @Override
337 public String toString() {
338 return getClass().getSimpleName() + " [" + state() + "]";
339 }
340
341 /**
342 * A change from one service state to another, plus the result of the change.
343 */
344 private class Transition extends AbstractFuture<State> {
345
346 @Override
347 public State get(long timeout, TimeUnit unit)
348 throws InterruptedException, TimeoutException, ExecutionException {
349 try {
350 return super.get(timeout, unit);
351 } catch (TimeoutException e) {
352 throw new TimeoutException(AbstractService.this.toString());
353 }
354 }
355 }
356
357 /**
358 * Attempts to execute all the listeners in {@link #queuedListeners} while not holding the
359 * {@link #lock}.
360 */
361 private void executeListeners() {
362 if (!lock.isHeldByCurrentThread()) {
363 synchronized (queuedListeners) {
364 Runnable listener;
365 while ((listener = queuedListeners.poll()) != null) {
366 listener.run();
367 }
368 }
369 }
370 }
371
372 @GuardedBy("lock")
373 private void starting() {
374 for (final ListenerExecutorPair pair : listeners) {
375 queuedListeners.add(new Runnable() {
376 public void run() {
377 pair.execute(new Runnable() {
378 public void run() {
379 pair.listener.starting();
380 }
381 });
382 }
383 });
384 }
385 }
386
387 @GuardedBy("lock")
388 private void running() {
389 for (final ListenerExecutorPair pair : listeners) {
390 queuedListeners.add(new Runnable() {
391 public void run() {
392 pair.execute(new Runnable() {
393 public void run() {
394 pair.listener.running();
395 }
396 });
397 }
398 });
399 }
400 }
401
402 @GuardedBy("lock")
403 private void stopping(final State from) {
404 for (final ListenerExecutorPair pair : listeners) {
405 queuedListeners.add(new Runnable() {
406 public void run() {
407 pair.execute(new Runnable() {
408 public void run() {
409 pair.listener.stopping(from);
410 }
411 });
412 }
413 });
414 }
415 }
416
417 @GuardedBy("lock")
418 private void terminated(final State from) {
419 for (final ListenerExecutorPair pair : listeners) {
420 queuedListeners.add(new Runnable() {
421 public void run() {
422 pair.execute(new Runnable() {
423 public void run() {
424 pair.listener.terminated(from);
425 }
426 });
427 }
428 });
429 }
430 // There are no more state transitions so we can clear this out.
431 listeners.clear();
432 }
433
434 @GuardedBy("lock")
435 private void failed(final State from, final Throwable cause) {
436 for (final ListenerExecutorPair pair : listeners) {
437 queuedListeners.add(new Runnable() {
438 public void run() {
439 pair.execute(new Runnable() {
440 public void run() {
441 pair.listener.failed(from, cause);
442 }
443 });
444 }
445 });
446 }
447 // There are no more state transitions so we can clear this out.
448 listeners.clear();
449 }
450
451 /** A simple holder for a listener and its executor. */
452 private static class ListenerExecutorPair {
453 final Listener listener;
454 final Executor executor;
455
456 ListenerExecutorPair(Listener listener, Executor executor) {
457 this.listener = listener;
458 this.executor = executor;
459 }
460
461 /**
462 * Executes the given {@link Runnable} on {@link #executor} logging and swallowing all
463 * exceptions
464 */
465 void execute(Runnable runnable) {
466 try {
467 executor.execute(runnable);
468 } catch (Exception e) {
469 logger.log(Level.SEVERE, "Exception while executing listener " + listener
470 + " with executor " + executor, e);
471 }
472 }
473 }
474
475 /**
476 * An immutable snapshot of the current state of the service. This class represents a consistent
477 * snapshot of the state and therefore it can be used to answer simple queries without needing to
478 * grab a lock.
479 */
480 @Immutable
481 private static final class StateSnapshot {
482 /**
483 * The internal state, which equals external state unless
484 * shutdownWhenStartupFinishes is true.
485 */
486 final State state;
487
488 /**
489 * If true, the user requested a shutdown while the service was still starting
490 * up.
491 */
492 final boolean shutdownWhenStartupFinishes;
493
494 /**
495 * The exception that caused this service to fail. This will be {@code null}
496 * unless the service has failed.
497 */
498 @Nullable
499 final Throwable failure;
500
501 StateSnapshot(State internalState) {
502 this(internalState, false, null);
503 }
504
505 StateSnapshot(State internalState, boolean shutdownWhenStartupFinishes, Throwable failure) {
506 checkArgument(!shutdownWhenStartupFinishes || internalState == State.STARTING,
507 "shudownWhenStartupFinishes can only be set if state is STARTING. Got %s instead.",
508 internalState);
509 checkArgument(!(failure != null ^ internalState == State.FAILED),
510 "A failure cause should be set if and only if the state is failed. Got %s and %s "
511 + "instead.", internalState, failure);
512 this.state = internalState;
513 this.shutdownWhenStartupFinishes = shutdownWhenStartupFinishes;
514 this.failure = failure;
515 }
516
517 /** @see Service#state() */
518 State externalState() {
519 if (shutdownWhenStartupFinishes && state == State.STARTING) {
520 return State.STOPPING;
521 } else {
522 return state;
523 }
524 }
525
526 /** @see Service#failureCause() */
527 Throwable failureCause() {
528 checkState(state == State.FAILED,
529 "failureCause() is only valid if the service has failed, service is %s", state);
530 return failure;
531 }
532 }
533 }