    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.util.concurrent.Service.State; // javadoc needs this
    023
    024    import java.util.concurrent.ExecutionException;
    025    import java.util.concurrent.TimeUnit;
    026    import java.util.concurrent.TimeoutException;
    027    import java.util.concurrent.locks.ReentrantLock;
    028
    029    /**
    030     * Base class for implementing services that can handle {@link #doStart} and
    031     * {@link #doStop} requests, responding to them with {@link #notifyStarted()}
    032     * and {@link #notifyStopped()} callbacks. Its subclasses must manage threads
    033     * manually; consider {@link AbstractExecutionThreadService} if you need only a
    034     * single execution thread.
    035     *
    036     * @author Jesse Wilson
    037     * @since 1.0
    038     */
    039    @Beta
    040    public abstract class AbstractService implements Service {
    041
    042      private final ReentrantLock lock = new ReentrantLock();
    043
    044      private final Transition startup = new Transition();
    045      private final Transition shutdown = new Transition();
    046
    047      /**
    048       * The internal state, which equals external state unless
    049       * shutdownWhenStartupFinishes is true. Guarded by {@code lock}.
    050       */
    051      private State state = State.NEW;
    052
    053      /**
    054       * If true, the user requested a shutdown while the service was still starting
    055       * up. Guarded by {@code lock}.
    056       */
    057      private boolean shutdownWhenStartupFinishes = false;
    058
    059      /**
    060       * This method is called by {@link #start} to initiate service startup. The
    061       * invocation of this method should cause a call to {@link #notifyStarted()},
    062       * either during this method's run, or after it has returned. If startup
    063       * fails, the invocation should cause a call to {@link
    064       * #notifyFailed(Throwable)} instead.
    065       *
    066       * <p>This method should return promptly; prefer to do work on a different
    067       * thread where it is convenient. It is invoked exactly once on service
    068       * startup, even when {@link #start} is called multiple times.
    069       */
    070      protected abstract void doStart();
    071
    072      /**
    073       * This method should be used to initiate service shutdown. The invocation
    074       * of this method should cause a call to {@link #notifyStopped()}, either
    075       * during this method's run, or after it has returned. If shutdown fails, the
    076       * invocation should cause a call to {@link #notifyFailed(Throwable)} instead.
    077       *
    078       * <p>This method should return promptly; prefer to do work on a different
    079       * thread where it is convenient. It is invoked exactly once on service
    080       * shutdown, even when {@link #stop} is called multiple times.
    081       */
    082      protected abstract void doStop();
    083
    084      @Override
    085      public final ListenableFuture<State> start() {
    086        lock.lock();
    087        try {
    088          if (state == State.NEW) {
    089            state = State.STARTING;
    090            doStart();
    091          }
    092        } catch (Throwable startupFailure) {
    093          // put the exception in the future, the user can get it via Future.get()
    094          notifyFailed(startupFailure);
    095        } finally {
    096          lock.unlock();
    097        }
    098
    099        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          }
    209
    210          state = State.FAILED;
    211        } finally {
    212          lock.unlock();
    213        }
    214      }
    215
    216      @Override
    217      public final boolean isRunning() {
    218        return state() == State.RUNNING;
    219      }
    220
    221      @Override
    222      public final State state() {
    223        lock.lock();
    224        try {
    225          if (shutdownWhenStartupFinishes && state == State.STARTING) {
    226            return State.STOPPING;
    227          } else {
    228            return state;
    229          }
    230        } finally {
    231          lock.unlock();
    232        }
    233      }
    234
    235      @Override public String toString() {
    236        return getClass().getSimpleName() + " [" + state() + "]";
    237      }
    238
    239      /**
    240       * A change from one service state to another, plus the result of the change.
    241       */
    242      private class Transition extends AbstractFuture<State> {
    243        @Override
    244        public State get(long timeout, TimeUnit unit)
    245            throws InterruptedException, TimeoutException, ExecutionException {
    246          try {
    247            return super.get(timeout, unit);
    248          } catch (TimeoutException e) {
    249            throw new TimeoutException(AbstractService.this.toString());
    250          }
    251        }
    252      }
    253    }