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
017package com.google.common.util.concurrent;
018
019import static com.google.common.base.Preconditions.checkNotNull;
020
021import com.google.common.annotations.Beta;
022import com.google.common.base.Throwables;
023import com.google.common.util.concurrent.Service.State; // javadoc needs this
024
025import java.util.concurrent.ExecutionException;
026import java.util.concurrent.TimeUnit;
027import java.util.concurrent.TimeoutException;
028import 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
041public 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}