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 import static com.google.common.base.Preconditions.checkState; 021 022 import com.google.common.annotations.Beta; 023 import com.google.common.collect.Lists; 024 import com.google.common.util.concurrent.Service.State; // javadoc needs this 025 026 import java.util.List; 027 import java.util.concurrent.ExecutionException; 028 import java.util.concurrent.Executor; 029 import java.util.concurrent.TimeUnit; 030 import java.util.concurrent.TimeoutException; 031 import java.util.concurrent.locks.ReentrantLock; 032 import java.util.logging.Level; 033 import java.util.logging.Logger; 034 035 import javax.annotation.Nullable; 036 import javax.annotation.concurrent.GuardedBy; 037 038 /** 039 * Base class for implementing services that can handle {@link #doStart} and 040 * {@link #doStop} requests, responding to them with {@link #notifyStarted()} 041 * and {@link #notifyStopped()} callbacks. Its subclasses must manage threads 042 * manually; consider {@link AbstractExecutionThreadService} if you need only a 043 * single execution thread. 044 * 045 * @author Jesse Wilson 046 * @since 1.0 047 */ 048 @Beta 049 public abstract class AbstractService implements Service { 050 private static final Logger logger = Logger.getLogger(AbstractService.class.getName()); 051 private final ReentrantLock lock = new ReentrantLock(); 052 053 private final Transition startup = new Transition(); 054 private final Transition shutdown = new Transition(); 055 056 /** 057 * The listeners to notify during a state transition. 058 */ 059 @GuardedBy("lock") 060 private final List<ListenerExecutorPair> listeners = Lists.newArrayList(); 061 062 /** 063 * The exception that caused this service to fail. This will be {@code null} 064 * unless the service has failed. 065 */ 066 @GuardedBy("lock") 067 @Nullable 068 private Throwable failure; 069 070 /** 071 * The internal state, which equals external state unless 072 * shutdownWhenStartupFinishes is true. 073 */ 074 @GuardedBy("lock") 075 private State state = State.NEW; 076 077 /** 078 * If true, the user requested a shutdown while the service was still starting 079 * up. 080 */ 081 @GuardedBy("lock") 082 private boolean shutdownWhenStartupFinishes = false; 083 084 /** 085 * This method is called by {@link #start} to initiate service startup. The 086 * invocation of this method should cause a call to {@link #notifyStarted()}, 087 * either during this method's run, or after it has returned. If startup 088 * fails, the invocation should cause a call to {@link 089 * #notifyFailed(Throwable)} instead. 090 * 091 * <p>This method should return promptly; prefer to do work on a different 092 * thread where it is convenient. It is invoked exactly once on service 093 * startup, even when {@link #start} is called multiple times. 094 */ 095 protected abstract void doStart(); 096 097 /** 098 * This method should be used to initiate service shutdown. The invocation 099 * of this method should cause a call to {@link #notifyStopped()}, either 100 * during this method's run, or after it has returned. If shutdown fails, the 101 * invocation should cause a call to {@link #notifyFailed(Throwable)} instead. 102 * 103 * <p>This method should return promptly; prefer to do work on a different 104 * thread where it is convenient. It is invoked exactly once on service 105 * shutdown, even when {@link #stop} is called multiple times. 106 */ 107 protected abstract void doStop(); 108 109 @Override 110 public final ListenableFuture<State> start() { 111 lock.lock(); 112 try { 113 if (state == State.NEW) { 114 starting(); 115 doStart(); 116 } 117 } catch (Throwable startupFailure) { 118 // put the exception in the future, the user can get it via Future.get() 119 notifyFailed(startupFailure); 120 } finally { 121 lock.unlock(); 122 } 123 124 return startup; 125 } 126 127 @Override 128 public final ListenableFuture<State> stop() { 129 lock.lock(); 130 try { 131 if (state == State.NEW) { 132 state = State.TERMINATED; 133 terminated(State.NEW); 134 startup.set(State.TERMINATED); 135 shutdown.set(State.TERMINATED); 136 } else if (state == State.STARTING) { 137 shutdownWhenStartupFinishes = true; 138 startup.set(State.STOPPING); 139 } else if (state == State.RUNNING) { 140 state = State.STOPPING; 141 stopping(State.RUNNING); 142 doStop(); 143 } 144 } catch (Throwable shutdownFailure) { 145 // put the exception in the future, the user can get it via Future.get() 146 notifyFailed(shutdownFailure); 147 } finally { 148 lock.unlock(); 149 } 150 151 return shutdown; 152 } 153 154 @Override 155 public State startAndWait() { 156 return Futures.getUnchecked(start()); 157 } 158 159 @Override 160 public State stopAndWait() { 161 return Futures.getUnchecked(stop()); 162 } 163 164 /** 165 * Implementing classes should invoke this method once their service has 166 * started. It will cause the service to transition from {@link 167 * State#STARTING} to {@link State#RUNNING}. 168 * 169 * @throws IllegalStateException if the service is not 170 * {@link State#STARTING}. 171 */ 172 protected final void notifyStarted() { 173 lock.lock(); 174 try { 175 if (state != State.STARTING) { 176 IllegalStateException failure = new IllegalStateException( 177 "Cannot notifyStarted() when the service is " + state); 178 notifyFailed(failure); 179 throw failure; 180 } 181 182 running(); 183 if (shutdownWhenStartupFinishes) { 184 stop(); 185 } else { 186 startup.set(State.RUNNING); 187 } 188 } finally { 189 lock.unlock(); 190 } 191 } 192 193 /** 194 * Implementing classes should invoke this method once their service has 195 * stopped. It will cause the service to transition from {@link 196 * State#STOPPING} to {@link State#TERMINATED}. 197 * 198 * @throws IllegalStateException if the service is neither {@link 199 * State#STOPPING} nor {@link State#RUNNING}. 200 */ 201 protected final void notifyStopped() { 202 lock.lock(); 203 try { 204 if (state != State.STOPPING && state != State.RUNNING) { 205 IllegalStateException failure = new IllegalStateException( 206 "Cannot notifyStopped() when the service is " + state); 207 notifyFailed(failure); 208 throw failure; 209 } 210 terminated(state); 211 shutdown.set(State.TERMINATED); 212 } finally { 213 lock.unlock(); 214 } 215 } 216 217 /** 218 * Invoke this method to transition the service to the 219 * {@link State#FAILED}. The service will <b>not be stopped</b> if it 220 * is running. Invoke this method when a service has failed critically or 221 * otherwise cannot be started nor stopped. 222 */ 223 protected final void notifyFailed(Throwable cause) { 224 checkNotNull(cause); 225 226 lock.lock(); 227 try { 228 if (state == State.STARTING) { 229 startup.setException(cause); 230 shutdown.setException(new Exception( 231 "Service failed to start.", cause)); 232 } else if (state == State.STOPPING) { 233 shutdown.setException(cause); 234 } else if (state == State.RUNNING) { 235 shutdown.setException( 236 new Exception("Service failed while running", cause)); 237 } else if (state == State.NEW || state == State.TERMINATED) { 238 throw new IllegalStateException( 239 "Failed while in state:" + state, cause); 240 } 241 failed(state, cause); 242 } finally { 243 lock.unlock(); 244 } 245 } 246 247 @Override 248 public final boolean isRunning() { 249 return state() == State.RUNNING; 250 } 251 252 @Override 253 public final State state() { 254 lock.lock(); 255 try { 256 if (shutdownWhenStartupFinishes && state == State.STARTING) { 257 return State.STOPPING; 258 } else { 259 return state; 260 } 261 } finally { 262 lock.unlock(); 263 } 264 } 265 266 @Override 267 public final Throwable failureCause() { 268 lock.lock(); 269 try { 270 checkState(state == State.FAILED, 271 "getFailure is only valid if the service has failed, service is %s", state); 272 return failure; 273 } finally { 274 lock.unlock(); 275 } 276 } 277 278 @Override 279 public final void addListener(Listener listener, Executor executor) { 280 checkNotNull(listener, "listener"); 281 checkNotNull(executor, "executor"); 282 lock.lock(); 283 try { 284 if (state != State.TERMINATED && state != State.FAILED) { 285 listeners.add(new ListenerExecutorPair(listener, executor)); 286 } 287 } finally { 288 lock.unlock(); 289 } 290 } 291 292 @Override public String toString() { 293 return getClass().getSimpleName() + " [" + state() + "]"; 294 } 295 296 /** 297 * A change from one service state to another, plus the result of the change. 298 */ 299 private class Transition extends AbstractFuture<State> { 300 @Override 301 public State get(long timeout, TimeUnit unit) 302 throws InterruptedException, TimeoutException, ExecutionException { 303 try { 304 return super.get(timeout, unit); 305 } catch (TimeoutException e) { 306 throw new TimeoutException(AbstractService.this.toString()); 307 } 308 } 309 } 310 311 @GuardedBy("lock") 312 private void starting() { 313 state = State.STARTING; 314 for (Listener listener : listeners) { 315 listener.starting(); 316 } 317 } 318 319 @GuardedBy("lock") 320 private void running() { 321 state = State.RUNNING; 322 for (Listener listener : listeners) { 323 listener.running(); 324 } 325 } 326 327 @GuardedBy("lock") 328 private void stopping(State from) { 329 state = State.STOPPING; 330 for (Listener listener : listeners) { 331 listener.stopping(from); 332 } 333 } 334 335 @GuardedBy("lock") 336 private void terminated(State from) { 337 state = State.TERMINATED; 338 for (Listener listener : listeners) { 339 listener.terminated(from); 340 } 341 // There are no more state transitions so we can clear this out. 342 listeners.clear(); 343 } 344 345 @GuardedBy("lock") 346 private void failed(State from, Throwable cause) { 347 failure = cause; 348 state = State.FAILED; 349 for (Listener listener : listeners) { 350 listener.failed(from, cause); 351 } 352 // There are no more state transitions so we can clear this out. 353 listeners.clear(); 354 } 355 356 /** 357 * A {@link Service.Listener} that schedules the callbacks of the delegate listener on an 358 * {@link Executor}. 359 */ 360 private static class ListenerExecutorPair implements Listener { 361 final Listener listener; 362 final Executor executor; 363 364 ListenerExecutorPair(Listener listener, Executor executor) { 365 this.listener = listener; 366 this.executor = executor; 367 } 368 369 /** 370 * Executes the given {@link Runnable} on {@link #executor} logging and swallowing all 371 * exceptions 372 */ 373 void execute(Runnable runnable) { 374 try { 375 executor.execute(runnable); 376 } catch (Exception e) { 377 logger.log(Level.SEVERE, "Exception while executing listener " + listener 378 + " with executor " + executor, e); 379 } 380 } 381 382 @Override 383 public void starting() { 384 execute(new Runnable() { 385 @Override 386 public void run() { 387 listener.starting(); 388 } 389 }); 390 } 391 392 @Override 393 public void running() { 394 execute(new Runnable() { 395 @Override 396 public void run() { 397 listener.running(); 398 } 399 }); 400 } 401 402 @Override 403 public void stopping(final State from) { 404 execute(new Runnable() { 405 @Override 406 public void run() { 407 listener.stopping(from); 408 } 409 }); 410 } 411 412 @Override 413 public void terminated(final State from) { 414 execute(new Runnable() { 415 @Override 416 public void run() { 417 listener.terminated(from); 418 } 419 }); 420 } 421 422 @Override 423 public void failed(final State from, final Throwable failure) { 424 execute(new Runnable() { 425 @Override 426 public void run() { 427 listener.failed(from, failure); 428 } 429 }); 430 } 431 } 432 }