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    }