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