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.checkNotNull;
020    import static com.google.common.base.Preconditions.checkState;
021    
022    import com.google.common.annotations.Beta;
023    import com.google.common.collect.Lists;
024    import com.google.common.util.concurrent.Service.State; // javadoc needs this
025    
026    import java.util.List;
027    import java.util.concurrent.ExecutionException;
028    import java.util.concurrent.Executor;
029    import java.util.concurrent.TimeUnit;
030    import java.util.concurrent.TimeoutException;
031    import java.util.concurrent.locks.ReentrantLock;
032    import java.util.logging.Level;
033    import java.util.logging.Logger;
034    
035    import javax.annotation.Nullable;
036    import javax.annotation.concurrent.GuardedBy;
037    
038    /**
039     * Base class for implementing services that can handle {@link #doStart} and
040     * {@link #doStop} requests, responding to them with {@link #notifyStarted()}
041     * and {@link #notifyStopped()} callbacks. Its subclasses must manage threads
042     * manually; consider {@link AbstractExecutionThreadService} if you need only a
043     * single execution thread.
044     *
045     * @author Jesse Wilson
046     * @since 1.0
047     */
048    @Beta
049    public abstract class AbstractService implements Service {
050      private static final Logger logger = Logger.getLogger(AbstractService.class.getName());
051      private final ReentrantLock lock = new ReentrantLock();
052    
053      private final Transition startup = new Transition();
054      private final Transition shutdown = new Transition();
055    
056      /**
057       * The listeners to notify during a state transition.
058       */
059      @GuardedBy("lock")
060      private final List<ListenerExecutorPair> listeners = Lists.newArrayList();
061      
062      /**
063       * The exception that caused this service to fail.  This will be {@code null}
064       * unless the service has failed.
065       */
066      @GuardedBy("lock")
067      @Nullable
068      private Throwable failure;
069      
070      /**
071       * The internal state, which equals external state unless
072       * shutdownWhenStartupFinishes is true.
073       */
074      @GuardedBy("lock")
075      private State state = State.NEW;
076    
077      /**
078       * If true, the user requested a shutdown while the service was still starting
079       * up.
080       */
081      @GuardedBy("lock")
082      private boolean shutdownWhenStartupFinishes = false;
083    
084      /**
085       * This method is called by {@link #start} to initiate service startup. The
086       * invocation of this method should cause a call to {@link #notifyStarted()},
087       * either during this method's run, or after it has returned. If startup
088       * fails, the invocation should cause a call to {@link
089       * #notifyFailed(Throwable)} instead.
090       *
091       * <p>This method should return promptly; prefer to do work on a different
092       * thread where it is convenient. It is invoked exactly once on service
093       * startup, even when {@link #start} is called multiple times.
094       */
095      protected abstract void doStart();
096    
097      /**
098       * This method should be used to initiate service shutdown. The invocation
099       * of this method should cause a call to {@link #notifyStopped()}, either
100       * during this method's run, or after it has returned. If shutdown fails, the
101       * invocation should cause a call to {@link #notifyFailed(Throwable)} instead.
102       *
103       * <p>This method should return promptly; prefer to do work on a different
104       * thread where it is convenient. It is invoked exactly once on service
105       * shutdown, even when {@link #stop} is called multiple times.
106       */
107      protected abstract void doStop();
108    
109      @Override
110      public final ListenableFuture<State> start() {
111        lock.lock();
112        try {
113          if (state == State.NEW) {
114            starting();
115            doStart();
116          }
117        } catch (Throwable startupFailure) {
118          // put the exception in the future, the user can get it via Future.get()
119          notifyFailed(startupFailure);
120        } finally {
121          lock.unlock();
122        }
123    
124        return startup;
125      }
126    
127      @Override
128      public final ListenableFuture<State> stop() {
129        lock.lock();
130        try {
131          if (state == State.NEW) {
132            state = State.TERMINATED;
133            terminated(State.NEW);
134            startup.set(State.TERMINATED);
135            shutdown.set(State.TERMINATED);
136          } else if (state == State.STARTING) {
137            shutdownWhenStartupFinishes = true;
138            startup.set(State.STOPPING);
139          } else if (state == State.RUNNING) {
140            state = State.STOPPING;
141            stopping(State.RUNNING);
142            doStop();
143          }
144        } catch (Throwable shutdownFailure) {
145          // put the exception in the future, the user can get it via Future.get()
146          notifyFailed(shutdownFailure);
147        } finally {
148          lock.unlock();
149        }
150    
151        return shutdown;
152      }
153    
154      @Override
155      public State startAndWait() {
156        return Futures.getUnchecked(start());
157      }
158    
159      @Override
160      public State stopAndWait() {
161        return Futures.getUnchecked(stop());
162      }
163    
164      /**
165       * Implementing classes should invoke this method once their service has
166       * started. It will cause the service to transition from {@link
167       * State#STARTING} to {@link State#RUNNING}.
168       *
169       * @throws IllegalStateException if the service is not
170       *     {@link State#STARTING}.
171       */
172      protected final void notifyStarted() {
173        lock.lock();
174        try {
175          if (state != State.STARTING) {
176            IllegalStateException failure = new IllegalStateException(
177                "Cannot notifyStarted() when the service is " + state);
178            notifyFailed(failure);
179            throw failure;
180          }
181    
182          running();
183          if (shutdownWhenStartupFinishes) {
184            stop();
185          } else {
186            startup.set(State.RUNNING);
187          }
188        } finally {
189          lock.unlock();
190        }
191      }
192    
193      /**
194       * Implementing classes should invoke this method once their service has
195       * stopped. It will cause the service to transition from {@link
196       * State#STOPPING} to {@link State#TERMINATED}.
197       *
198       * @throws IllegalStateException if the service is neither {@link
199       *     State#STOPPING} nor {@link State#RUNNING}.
200       */
201      protected final void notifyStopped() {
202        lock.lock();
203        try {
204          if (state != State.STOPPING && state != State.RUNNING) {
205            IllegalStateException failure = new IllegalStateException(
206                "Cannot notifyStopped() when the service is " + state);
207            notifyFailed(failure);
208            throw failure;
209          }
210          terminated(state);
211          shutdown.set(State.TERMINATED);
212        } finally {
213          lock.unlock();
214        }
215      }
216    
217      /**
218       * Invoke this method to transition the service to the
219       * {@link State#FAILED}. The service will <b>not be stopped</b> if it
220       * is running. Invoke this method when a service has failed critically or
221       * otherwise cannot be started nor stopped.
222       */
223      protected final void notifyFailed(Throwable cause) {
224        checkNotNull(cause);
225    
226        lock.lock();
227        try {
228          if (state == State.STARTING) {
229            startup.setException(cause);
230            shutdown.setException(new Exception(
231                "Service failed to start.", cause));
232          } else if (state == State.STOPPING) {
233            shutdown.setException(cause);
234          } else if (state == State.RUNNING) {
235            shutdown.setException(
236                new Exception("Service failed while running", cause));
237          } else if (state == State.NEW || state == State.TERMINATED) {
238            throw new IllegalStateException(
239                "Failed while in state:" + state, cause);
240          }
241          failed(state, cause);
242        } finally {
243          lock.unlock();
244        }
245      }
246    
247      @Override
248      public final boolean isRunning() {
249        return state() == State.RUNNING;
250      }
251    
252      @Override
253      public final State state() {
254        lock.lock();
255        try {
256          if (shutdownWhenStartupFinishes && state == State.STARTING) {
257            return State.STOPPING;
258          } else {
259            return state;
260          }
261        } finally {
262          lock.unlock();
263        }
264      }
265      
266      @Override
267      public final Throwable failureCause() {
268        lock.lock();
269        try {
270          checkState(state == State.FAILED, 
271              "getFailure is only valid if the service has failed, service is %s", state);
272          return failure;
273        } finally {
274          lock.unlock();
275        }
276      }
277      
278      @Override
279      public final void addListener(Listener listener, Executor executor) {
280        checkNotNull(listener, "listener");
281        checkNotNull(executor, "executor");
282        lock.lock();
283        try {
284          if (state != State.TERMINATED && state != State.FAILED) {
285            listeners.add(new ListenerExecutorPair(listener, executor));
286          }
287        } finally {
288          lock.unlock();
289        }
290      }
291    
292      @Override public String toString() {
293        return getClass().getSimpleName() + " [" + state() + "]";
294      }
295    
296      /**
297       * A change from one service state to another, plus the result of the change.
298       */
299      private class Transition extends AbstractFuture<State> {
300        @Override
301        public State get(long timeout, TimeUnit unit)
302            throws InterruptedException, TimeoutException, ExecutionException {
303          try {
304            return super.get(timeout, unit);
305          } catch (TimeoutException e) {
306            throw new TimeoutException(AbstractService.this.toString());
307          }
308        }
309      }
310      
311      @GuardedBy("lock")
312      private void starting() {
313        state = State.STARTING;
314        for (Listener listener : listeners) {
315          listener.starting();
316        }
317      }
318    
319      @GuardedBy("lock")
320      private void running() {
321        state = State.RUNNING;
322        for (Listener listener : listeners) {
323          listener.running();
324        }
325      }
326    
327      @GuardedBy("lock")
328      private void stopping(State from) {
329        state = State.STOPPING;
330        for (Listener listener : listeners) {
331          listener.stopping(from);
332        }
333      }
334    
335      @GuardedBy("lock")
336      private void terminated(State from) {
337        state = State.TERMINATED;
338        for (Listener listener : listeners) {
339          listener.terminated(from);
340        }
341        // There are no more state transitions so we can clear this out.
342        listeners.clear();
343      }
344    
345      @GuardedBy("lock")
346      private void failed(State from, Throwable cause) {
347        failure = cause;
348        state = State.FAILED;
349        for (Listener listener : listeners) {
350          listener.failed(from, cause);
351        }
352        // There are no more state transitions so we can clear this out.
353        listeners.clear();
354      }
355      
356      /** 
357       * A {@link Service.Listener} that schedules the callbacks of the delegate listener on an 
358       * {@link Executor}.
359       */
360      private static class ListenerExecutorPair implements Listener {
361        final Listener listener;
362        final Executor executor;
363    
364        ListenerExecutorPair(Listener listener, Executor executor) {
365          this.listener = listener;
366          this.executor = executor;
367        }
368    
369        /**
370         * Executes the given {@link Runnable} on {@link #executor} logging and swallowing all 
371         * exceptions
372         */
373        void execute(Runnable runnable) {
374          try {
375            executor.execute(runnable);
376          } catch (Exception e) {
377            logger.log(Level.SEVERE, "Exception while executing listener " + listener 
378                + " with executor " + executor, e);
379          }
380        }
381    
382        @Override
383        public void starting() {
384          execute(new Runnable() {
385            @Override
386            public void run() {
387              listener.starting();
388            }
389          });
390        }
391    
392        @Override
393        public void running() {
394          execute(new Runnable() {
395            @Override
396            public void run() {
397              listener.running();
398            }
399          });
400        }
401    
402        @Override
403        public void stopping(final State from) {
404          execute(new Runnable() {
405            @Override
406            public void run() {
407              listener.stopping(from);
408            }
409          });
410        }
411    
412        @Override
413        public void terminated(final State from) {
414          execute(new Runnable() {
415            @Override
416            public void run() {
417              listener.terminated(from);
418            }
419          });
420        }
421    
422        @Override
423        public void failed(final State from, final Throwable failure) {
424          execute(new Runnable() {
425            @Override
426            public void run() {
427              listener.failed(from, failure);
428            }
429          });
430        }
431      }
432    }