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 }