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 }