All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dev.mccue.guava.concurrent.AbstractService Maven / Gradle / Ivy

There is a newer version: 33.2.0
Show newest version
/*
 * Copyright (C) 2009 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package dev.mccue.guava.concurrent;

import static dev.mccue.guava.base.Preconditions.checkArgument;
import static dev.mccue.guava.base.Preconditions.checkNotNull;
import static dev.mccue.guava.base.Preconditions.checkState;
import static dev.mccue.guava.concurrent.Platform.restoreInterruptIfIsInterruptedException;
import static dev.mccue.guava.concurrent.Service.State.FAILED;
import static dev.mccue.guava.concurrent.Service.State.NEW;
import static dev.mccue.guava.concurrent.Service.State.RUNNING;
import static dev.mccue.guava.concurrent.Service.State.STARTING;
import static dev.mccue.guava.concurrent.Service.State.STOPPING;
import static dev.mccue.guava.concurrent.Service.State.TERMINATED;
import static java.util.Objects.requireNonNull;

import dev.mccue.guava.concurrent.Monitor.Guard;
import dev.mccue.guava.concurrent.Service.State; // javadoc needs this
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.ForOverride;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import java.time.Duration;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import dev.mccue.jsr305.CheckForNull;

/**
 * Base class for implementing services that can handle {@code #doStart} and {@code #doStop}
 * requests, responding to them with {@code #notifyStarted()} and {@code #notifyStopped()}
 * callbacks. Its subclasses must manage threads manually; consider {@code
 * AbstractExecutionThreadService} if you need only a single execution thread.
 *
 * @author Jesse Wilson
 * @author Luke Sandberg
 * @since 1.0
 */
@ElementTypesAreNonnullByDefault
public abstract class AbstractService implements Service {
  private static final ListenerCallQueue.Event STARTING_EVENT =
      new ListenerCallQueue.Event() {
        @Override
        public void call(Listener listener) {
          listener.starting();
        }

        @Override
        public String toString() {
          return "starting()";
        }
      };
  private static final ListenerCallQueue.Event RUNNING_EVENT =
      new ListenerCallQueue.Event() {
        @Override
        public void call(Listener listener) {
          listener.running();
        }

        @Override
        public String toString() {
          return "running()";
        }
      };
  private static final ListenerCallQueue.Event STOPPING_FROM_STARTING_EVENT =
      stoppingEvent(STARTING);
  private static final ListenerCallQueue.Event STOPPING_FROM_RUNNING_EVENT =
      stoppingEvent(RUNNING);

  private static final ListenerCallQueue.Event TERMINATED_FROM_NEW_EVENT =
      terminatedEvent(NEW);
  private static final ListenerCallQueue.Event TERMINATED_FROM_STARTING_EVENT =
      terminatedEvent(STARTING);
  private static final ListenerCallQueue.Event TERMINATED_FROM_RUNNING_EVENT =
      terminatedEvent(RUNNING);
  private static final ListenerCallQueue.Event TERMINATED_FROM_STOPPING_EVENT =
      terminatedEvent(STOPPING);

  private static ListenerCallQueue.Event terminatedEvent(final State from) {
    return new ListenerCallQueue.Event() {
      @Override
      public void call(Listener listener) {
        listener.terminated(from);
      }

      @Override
      public String toString() {
        return "terminated({from = " + from + "})";
      }
    };
  }

  private static ListenerCallQueue.Event stoppingEvent(final State from) {
    return new ListenerCallQueue.Event() {
      @Override
      public void call(Listener listener) {
        listener.stopping(from);
      }

      @Override
      public String toString() {
        return "stopping({from = " + from + "})";
      }
    };
  }

  private final Monitor monitor = new Monitor();

  private final Guard isStartable = new IsStartableGuard();

  private final class IsStartableGuard extends Guard {
    IsStartableGuard() {
      super(AbstractService.this.monitor);
    }

    @Override
    public boolean isSatisfied() {
      return state() == NEW;
    }
  }

  private final Guard isStoppable = new IsStoppableGuard();

  private final class IsStoppableGuard extends Guard {
    IsStoppableGuard() {
      super(AbstractService.this.monitor);
    }

    @Override
    public boolean isSatisfied() {
      return state().compareTo(RUNNING) <= 0;
    }
  }

  private final Guard hasReachedRunning = new HasReachedRunningGuard();

  private final class HasReachedRunningGuard extends Guard {
    HasReachedRunningGuard() {
      super(AbstractService.this.monitor);
    }

    @Override
    public boolean isSatisfied() {
      return state().compareTo(RUNNING) >= 0;
    }
  }

  private final Guard isStopped = new IsStoppedGuard();

  private final class IsStoppedGuard extends Guard {
    IsStoppedGuard() {
      super(AbstractService.this.monitor);
    }

    @Override
    public boolean isSatisfied() {
      return state().compareTo(TERMINATED) >= 0;
    }
  }

  /** The listeners to notify during a state transition. */
  private final ListenerCallQueue listeners = new ListenerCallQueue<>();

  /**
   * The current state of the service. This should be written with the lock held but can be read
   * without it because it is an immutable object in a volatile field. This is desirable so that
   * methods like {@code #state}, {@code #failureCause} and notably {@code #toString} can be run
   * without grabbing the lock.
   *
   * 

To update this field correctly the lock must be held to guarantee that the state is * consistent. */ private volatile StateSnapshot snapshot = new StateSnapshot(NEW); /** Constructor for use by subclasses. */ protected AbstractService() {} /** * This method is called by {@code #startAsync} to initiate service startup. The invocation of * this method should cause a call to {@code #notifyStarted()}, either during this method's run, * or after it has returned. If startup fails, the invocation should cause a call to {@code * #notifyFailed(Throwable)} instead. * *

This method should return promptly; prefer to do work on a different thread where it is * convenient. It is invoked exactly once on service startup, even when {@code #startAsync} is * called multiple times. */ @ForOverride protected abstract void doStart(); /** * This method should be used to initiate service shutdown. The invocation of this method should * cause a call to {@code #notifyStopped()}, either during this method's run, or after it has * returned. If shutdown fails, the invocation should cause a call to {@code * #notifyFailed(Throwable)} instead. * *

This method should return promptly; prefer to do work on a different thread where it is * convenient. It is invoked exactly once on service shutdown, even when {@code #stopAsync} is * called multiple times. * *

If {@code #stopAsync} is called on a {@code State#STARTING} service, this method is not * invoked immediately. Instead, it will be deferred until after the service is {@code * State#RUNNING}. Services that need to cancel startup work can override {@code #doCancelStart}. */ @ForOverride protected abstract void doStop(); /** * This method is called by {@code #stopAsync} when the service is still starting (i.e. {@code * #startAsync} has been called but {@code #notifyStarted} has not). Subclasses can override the * method to cancel pending work and then call {@code #notifyStopped} to stop the service. * *

This method should return promptly; prefer to do work on a different thread where it is * convenient. It is invoked exactly once on service shutdown, even when {@code #stopAsync} is * called multiple times. * *

When this method is called {@code #state()} will return {@code State#STOPPING}, which is the * external state observable by the caller of {@code #stopAsync}. * * @since 27.0 */ @ForOverride protected void doCancelStart() {} @CanIgnoreReturnValue @Override public final Service startAsync() { if (monitor.enterIf(isStartable)) { try { snapshot = new StateSnapshot(STARTING); enqueueStartingEvent(); doStart(); } catch (Throwable startupFailure) { restoreInterruptIfIsInterruptedException(startupFailure); notifyFailed(startupFailure); } finally { monitor.leave(); dispatchListenerEvents(); } } else { throw new IllegalStateException("Service " + this + " has already been started"); } return this; } @CanIgnoreReturnValue @Override public final Service stopAsync() { if (monitor.enterIf(isStoppable)) { try { State previous = state(); switch (previous) { case NEW: snapshot = new StateSnapshot(TERMINATED); enqueueTerminatedEvent(NEW); break; case STARTING: snapshot = new StateSnapshot(STARTING, true, null); enqueueStoppingEvent(STARTING); doCancelStart(); break; case RUNNING: snapshot = new StateSnapshot(STOPPING); enqueueStoppingEvent(RUNNING); doStop(); break; case STOPPING: case TERMINATED: case FAILED: // These cases are impossible due to the if statement above. throw new AssertionError("isStoppable is incorrectly implemented, saw: " + previous); } } catch (Throwable shutdownFailure) { restoreInterruptIfIsInterruptedException(shutdownFailure); notifyFailed(shutdownFailure); } finally { monitor.leave(); dispatchListenerEvents(); } } return this; } @Override public final void awaitRunning() { monitor.enterWhenUninterruptibly(hasReachedRunning); try { checkCurrentState(RUNNING); } finally { monitor.leave(); } } /** @since 28.0 */ @Override public final void awaitRunning(Duration timeout) throws TimeoutException { Service.super.awaitRunning(timeout); } @Override public final void awaitRunning(long timeout, TimeUnit unit) throws TimeoutException { if (monitor.enterWhenUninterruptibly(hasReachedRunning, timeout, unit)) { try { checkCurrentState(RUNNING); } finally { monitor.leave(); } } else { // It is possible due to races that we are currently in the expected state even though we // timed out. e.g. if we weren't event able to grab the lock within the timeout we would never // even check the guard. I don't think we care too much about this use case but it could lead // to a confusing error message. throw new TimeoutException("Timed out waiting for " + this + " to reach the RUNNING state."); } } @Override public final void awaitTerminated() { monitor.enterWhenUninterruptibly(isStopped); try { checkCurrentState(TERMINATED); } finally { monitor.leave(); } } /** @since 28.0 */ @Override public final void awaitTerminated(Duration timeout) throws TimeoutException { Service.super.awaitTerminated(timeout); } @Override public final void awaitTerminated(long timeout, TimeUnit unit) throws TimeoutException { if (monitor.enterWhenUninterruptibly(isStopped, timeout, unit)) { try { checkCurrentState(TERMINATED); } finally { monitor.leave(); } } else { // It is possible due to races that we are currently in the expected state even though we // timed out. e.g. if we weren't event able to grab the lock within the timeout we would never // even check the guard. I don't think we care too much about this use case but it could lead // to a confusing error message. throw new TimeoutException( "Timed out waiting for " + this + " to reach a terminal state. " + "Current state: " + state()); } } /** Checks that the current state is equal to the expected state. */ @GuardedBy("monitor") private void checkCurrentState(State expected) { State actual = state(); if (actual != expected) { if (actual == FAILED) { // Handle this specially so that we can include the failureCause, if there is one. throw new IllegalStateException( "Expected the service " + this + " to be " + expected + ", but the service has FAILED", failureCause()); } throw new IllegalStateException( "Expected the service " + this + " to be " + expected + ", but was " + actual); } } /** * Implementing classes should invoke this method once their service has started. It will cause * the service to transition from {@code State#STARTING} to {@code State#RUNNING}. * * @throws IllegalStateException if the service is not {@code State#STARTING}. */ protected final void notifyStarted() { monitor.enter(); try { // We have to examine the internal state of the snapshot here to properly handle the stop // while starting case. if (snapshot.state != STARTING) { IllegalStateException failure = new IllegalStateException( "Cannot notifyStarted() when the service is " + snapshot.state); notifyFailed(failure); throw failure; } if (snapshot.shutdownWhenStartupFinishes) { snapshot = new StateSnapshot(STOPPING); // We don't call listeners here because we already did that when we set the // shutdownWhenStartupFinishes flag. doStop(); } else { snapshot = new StateSnapshot(RUNNING); enqueueRunningEvent(); } } finally { monitor.leave(); dispatchListenerEvents(); } } /** * Implementing classes should invoke this method once their service has stopped. It will cause * the service to transition from {@code State#STARTING} or {@code State#STOPPING} to {@code * State#TERMINATED}. * * @throws IllegalStateException if the service is not one of {@code State#STOPPING}, {@code * State#STARTING}, or {@code State#RUNNING}. */ protected final void notifyStopped() { monitor.enter(); try { State previous = state(); switch (previous) { case NEW: case TERMINATED: case FAILED: throw new IllegalStateException("Cannot notifyStopped() when the service is " + previous); case RUNNING: case STARTING: case STOPPING: snapshot = new StateSnapshot(TERMINATED); enqueueTerminatedEvent(previous); break; } } finally { monitor.leave(); dispatchListenerEvents(); } } /** * Invoke this method to transition the service to the {@code State#FAILED}. The service will * not be stopped if it is running. Invoke this method when a service has failed critically * or otherwise cannot be started nor stopped. */ protected final void notifyFailed(Throwable cause) { checkNotNull(cause); monitor.enter(); try { State previous = state(); switch (previous) { case NEW: case TERMINATED: throw new IllegalStateException("Failed while in state:" + previous, cause); case RUNNING: case STARTING: case STOPPING: snapshot = new StateSnapshot(FAILED, false, cause); enqueueFailedEvent(previous, cause); break; case FAILED: // Do nothing break; } } finally { monitor.leave(); dispatchListenerEvents(); } } @Override public final boolean isRunning() { return state() == RUNNING; } @Override public final State state() { return snapshot.externalState(); } /** @since 14.0 */ @Override public final Throwable failureCause() { return snapshot.failureCause(); } /** @since 13.0 */ @Override public final void addListener(Listener listener, Executor executor) { listeners.addListener(listener, executor); } @Override public String toString() { return getClass().getSimpleName() + " [" + state() + "]"; } /** * Attempts to execute all the listeners in {@code #listeners} while not holding the {@code * #monitor}. */ private void dispatchListenerEvents() { if (!monitor.isOccupiedByCurrentThread()) { listeners.dispatch(); } } private void enqueueStartingEvent() { listeners.enqueue(STARTING_EVENT); } private void enqueueRunningEvent() { listeners.enqueue(RUNNING_EVENT); } private void enqueueStoppingEvent(final State from) { if (from == State.STARTING) { listeners.enqueue(STOPPING_FROM_STARTING_EVENT); } else if (from == State.RUNNING) { listeners.enqueue(STOPPING_FROM_RUNNING_EVENT); } else { throw new AssertionError(); } } private void enqueueTerminatedEvent(final State from) { switch (from) { case NEW: listeners.enqueue(TERMINATED_FROM_NEW_EVENT); break; case STARTING: listeners.enqueue(TERMINATED_FROM_STARTING_EVENT); break; case RUNNING: listeners.enqueue(TERMINATED_FROM_RUNNING_EVENT); break; case STOPPING: listeners.enqueue(TERMINATED_FROM_STOPPING_EVENT); break; case TERMINATED: case FAILED: throw new AssertionError(); } } private void enqueueFailedEvent(final State from, final Throwable cause) { // can't memoize this one due to the exception listeners.enqueue( new ListenerCallQueue.Event() { @Override public void call(Listener listener) { listener.failed(from, cause); } @Override public String toString() { return "failed({from = " + from + ", cause = " + cause + "})"; } }); } /** * An immutable snapshot of the current state of the service. This class represents a consistent * snapshot of the state and therefore it can be used to answer simple queries without needing to * grab a lock. */ // @Immutable except that Throwable is mutable (initCause(), setStackTrace(), mutable subclasses). private static final class StateSnapshot { /** * The internal state, which equals external state unless shutdownWhenStartupFinishes is true. */ final State state; /** If true, the user requested a shutdown while the service was still starting up. */ final boolean shutdownWhenStartupFinishes; /** * The exception that caused this service to fail. This will be {@code null} unless the service * has failed. */ @CheckForNull final Throwable failure; StateSnapshot(State internalState) { this(internalState, false, null); } StateSnapshot( State internalState, boolean shutdownWhenStartupFinishes, @CheckForNull Throwable failure) { checkArgument( !shutdownWhenStartupFinishes || internalState == STARTING, "shutdownWhenStartupFinishes can only be set if state is STARTING. Got %s instead.", internalState); checkArgument( (failure != null) == (internalState == FAILED), "A failure cause should be set if and only if the state is failed. Got %s and %s " + "instead.", internalState, failure); this.state = internalState; this.shutdownWhenStartupFinishes = shutdownWhenStartupFinishes; this.failure = failure; } /** @see Service#state() */ State externalState() { if (shutdownWhenStartupFinishes && state == STARTING) { return STOPPING; } else { return state; } } /** @see Service#failureCause() */ Throwable failureCause() { checkState( state == FAILED, "failureCause() is only valid if the service has failed, service is %s", state); // requireNonNull is safe because the constructor requires a non-null cause with state=FAILED. return requireNonNull(failure); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy