Home | History | Annotate | Download | only in concurrent
      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 
     17 package com.google.common.util.concurrent;
     18 
     19 import static com.google.common.base.Preconditions.checkNotNull;
     20 
     21 import com.google.common.annotations.Beta;
     22 import com.google.common.util.concurrent.Service.State; // javadoc needs this
     23 
     24 import java.util.concurrent.ExecutionException;
     25 import java.util.concurrent.TimeUnit;
     26 import java.util.concurrent.TimeoutException;
     27 import 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
     40 public 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