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    
021    import com.google.common.annotations.Beta;
022    import com.google.common.util.concurrent.Service.State; // javadoc needs this
023    
024    import java.util.concurrent.ExecutionException;
025    import java.util.concurrent.TimeUnit;
026    import java.util.concurrent.TimeoutException;
027    import java.util.concurrent.locks.ReentrantLock;
028    
029    /**
030     * Base class for implementing services that can handle {@link #doStart} and
031     * {@link #doStop} requests, responding to them with {@link #notifyStarted()}
032     * and {@link #notifyStopped()} callbacks. Its subclasses must manage threads
033     * manually; consider {@link AbstractExecutionThreadService} if you need only a
034     * single execution thread.
035     *
036     * @author Jesse Wilson
037     * @since 1.0
038     */
039    @Beta
040    public abstract class AbstractService implements Service {
041    
042      private final ReentrantLock lock = new ReentrantLock();
043    
044      private final Transition startup = new Transition();
045      private final Transition shutdown = new Transition();
046    
047      /**
048       * The internal state, which equals external state unless
049       * shutdownWhenStartupFinishes is true. Guarded by {@code lock}.
050       */
051      private State state = State.NEW;
052    
053      /**
054       * If true, the user requested a shutdown while the service was still starting
055       * up. Guarded by {@code lock}.
056       */
057      private boolean shutdownWhenStartupFinishes = false;
058    
059      /**
060       * This method is called by {@link #start} to initiate service startup. The
061       * invocation of this method should cause a call to {@link #notifyStarted()},
062       * either during this method's run, or after it has returned. If startup
063       * fails, the invocation should cause a call to {@link
064       * #notifyFailed(Throwable)} instead.
065       *
066       * <p>This method should return promptly; prefer to do work on a different
067       * thread where it is convenient. It is invoked exactly once on service
068       * startup, even when {@link #start} is called multiple times.
069       */
070      protected abstract void doStart();
071    
072      /**
073       * This method should be used to initiate service shutdown. The invocation
074       * of this method should cause a call to {@link #notifyStopped()}, either
075       * during this method's run, or after it has returned. If shutdown fails, the
076       * invocation should cause a call to {@link #notifyFailed(Throwable)} instead.
077       *
078       * <p>This method should return promptly; prefer to do work on a different
079       * thread where it is convenient. It is invoked exactly once on service
080       * shutdown, even when {@link #stop} is called multiple times.
081       */
082      protected abstract void doStop();
083    
084      @Override
085      public final ListenableFuture<State> start() {
086        lock.lock();
087        try {
088          if (state == State.NEW) {
089            state = State.STARTING;
090            doStart();
091          }
092        } catch (Throwable startupFailure) {
093          // put the exception in the future, the user can get it via Future.get()
094          notifyFailed(startupFailure);
095        } finally {
096          lock.unlock();
097        }
098    
099        return startup;
100      }
101    
102      @Override
103      public final ListenableFuture<State> stop() {
104        lock.lock();
105        try {
106          if (state == State.NEW) {
107            state = State.TERMINATED;
108            startup.set(State.TERMINATED);
109            shutdown.set(State.TERMINATED);
110          } else if (state == State.STARTING) {
111            shutdownWhenStartupFinishes = true;
112            startup.set(State.STOPPING);
113          } else if (state == State.RUNNING) {
114            state = State.STOPPING;
115            doStop();
116          }
117        } catch (Throwable shutdownFailure) {
118          // put the exception in the future, the user can get it via Future.get()
119          notifyFailed(shutdownFailure);
120        } finally {
121          lock.unlock();
122        }
123    
124        return shutdown;
125      }
126    
127      @Override
128      public State startAndWait() {
129        return Futures.getUnchecked(start());
130      }
131    
132      @Override
133      public State stopAndWait() {
134        return Futures.getUnchecked(stop());
135      }
136    
137      /**
138       * Implementing classes should invoke this method once their service has
139       * started. It will cause the service to transition from {@link
140       * State#STARTING} to {@link State#RUNNING}.
141       *
142       * @throws IllegalStateException if the service is not
143       *     {@link State#STARTING}.
144       */
145      protected final void notifyStarted() {
146        lock.lock();
147        try {
148          if (state != State.STARTING) {
149            IllegalStateException failure = new IllegalStateException(
150                "Cannot notifyStarted() when the service is " + state);
151            notifyFailed(failure);
152            throw failure;
153          }
154    
155          state = State.RUNNING;
156          if (shutdownWhenStartupFinishes) {
157            stop();
158          } else {
159            startup.set(State.RUNNING);
160          }
161        } finally {
162          lock.unlock();
163        }
164      }
165    
166      /**
167       * Implementing classes should invoke this method once their service has
168       * stopped. It will cause the service to transition from {@link
169       * State#STOPPING} to {@link State#TERMINATED}.
170       *
171       * @throws IllegalStateException if the service is neither {@link
172       *     State#STOPPING} nor {@link State#RUNNING}.
173       */
174      protected final void notifyStopped() {
175        lock.lock();
176        try {
177          if (state != State.STOPPING && state != State.RUNNING) {
178            IllegalStateException failure = new IllegalStateException(
179                "Cannot notifyStopped() when the service is " + state);
180            notifyFailed(failure);
181            throw failure;
182          }
183    
184          state = State.TERMINATED;
185          shutdown.set(State.TERMINATED);
186        } finally {
187          lock.unlock();
188        }
189      }
190    
191      /**
192       * Invoke this method to transition the service to the
193       * {@link State#FAILED}. The service will <b>not be stopped</b> if it
194       * is running. Invoke this method when a service has failed critically or
195       * otherwise cannot be started nor stopped.
196       */
197      protected final void notifyFailed(Throwable cause) {
198        checkNotNull(cause);
199    
200        lock.lock();
201        try {
202          if (state == State.STARTING) {
203            startup.setException(cause);
204            shutdown.setException(new Exception(
205                "Service failed to start.", cause));
206          } else if (state == State.STOPPING) {
207            shutdown.setException(cause);
208          } else if (state == State.RUNNING) {
209            shutdown.setException(new Exception("Service failed while running", cause));
210          } else if (state == State.NEW || state == State.TERMINATED) {
211            throw new IllegalStateException("Failed while in state:" + state, cause);
212          }
213          state = State.FAILED;
214        } finally {
215          lock.unlock();
216        }
217      }
218    
219      @Override
220      public final boolean isRunning() {
221        return state() == State.RUNNING;
222      }
223    
224      @Override
225      public final State state() {
226        lock.lock();
227        try {
228          if (shutdownWhenStartupFinishes && state == State.STARTING) {
229            return State.STOPPING;
230          } else {
231            return state;
232          }
233        } finally {
234          lock.unlock();
235        }
236      }
237    
238      @Override public String toString() {
239        return getClass().getSimpleName() + " [" + state() + "]";
240      }
241    
242      /**
243       * A change from one service state to another, plus the result of the change.
244       */
245      private class Transition extends AbstractFuture<State> {
246        @Override
247        public State get(long timeout, TimeUnit unit)
248            throws InterruptedException, TimeoutException, ExecutionException {
249          try {
250            return super.get(timeout, unit);
251          } catch (TimeoutException e) {
252            throw new TimeoutException(AbstractService.this.toString());
253          }
254        }
255      }
256    }