1/*
2 * Copyright (C) 2009 The Guava Authors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.common.util.concurrent;
18
19import static com.google.common.base.Preconditions.checkNotNull;
20
21import com.google.common.annotations.Beta;
22import com.google.common.util.concurrent.Service.State; // javadoc needs this
23
24import java.util.concurrent.ExecutionException;
25import java.util.concurrent.TimeUnit;
26import java.util.concurrent.TimeoutException;
27import java.util.concurrent.locks.ReentrantLock;
28
29/**
30 * Base class for implementing services that can handle {@link #doStart} and
31 * {@link #doStop} requests, responding to them with {@link #notifyStarted()}
32 * and {@link #notifyStopped()} callbacks. Its subclasses must manage threads
33 * manually; consider {@link AbstractExecutionThreadService} if you need only a
34 * single execution thread.
35 *
36 * @author Jesse Wilson
37 * @since 1.0
38 */
39@Beta
40public abstract class AbstractService implements Service {
41
42  private final ReentrantLock lock = new ReentrantLock();
43
44  private final Transition startup = new Transition();
45  private final Transition shutdown = new Transition();
46
47  /**
48   * The internal state, which equals external state unless
49   * shutdownWhenStartupFinishes is true. Guarded by {@code lock}.
50   */
51  private State state = State.NEW;
52
53  /**
54   * If true, the user requested a shutdown while the service was still starting
55   * up. Guarded by {@code lock}.
56   */
57  private boolean shutdownWhenStartupFinishes = false;
58
59  /**
60   * This method is called by {@link #start} to initiate service startup. The
61   * invocation of this method should cause a call to {@link #notifyStarted()},
62   * either during this method's run, or after it has returned. If startup
63   * fails, the invocation should cause a call to {@link
64   * #notifyFailed(Throwable)} instead.
65   *
66   * <p>This method should return promptly; prefer to do work on a different
67   * thread where it is convenient. It is invoked exactly once on service
68   * startup, even when {@link #start} is called multiple times.
69   */
70  protected abstract void doStart();
71
72  /**
73   * This method should be used to initiate service shutdown. The invocation
74   * of this method should cause a call to {@link #notifyStopped()}, either
75   * during this method's run, or after it has returned. If shutdown fails, the
76   * invocation should cause a call to {@link #notifyFailed(Throwable)} instead.
77   *
78   * <p>This method should return promptly; prefer to do work on a different
79   * thread where it is convenient. It is invoked exactly once on service
80   * shutdown, even when {@link #stop} is called multiple times.
81   */
82  protected abstract void doStop();
83
84  @Override
85  public final ListenableFuture<State> start() {
86    lock.lock();
87    try {
88      if (state == State.NEW) {
89        state = State.STARTING;
90        doStart();
91      }
92    } catch (Throwable startupFailure) {
93      // put the exception in the future, the user can get it via Future.get()
94      notifyFailed(startupFailure);
95    } finally {
96      lock.unlock();
97    }
98
99    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}
257