/*
* 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 com.google.common.util.concurrent;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.util.concurrent.Service.State.FAILED;
import static com.google.common.util.concurrent.Service.State.NEW;
import static com.google.common.util.concurrent.Service.State.RUNNING;
import static com.google.common.util.concurrent.Service.State.STARTING;
import static com.google.common.util.concurrent.Service.State.STOPPING;
import static com.google.common.util.concurrent.Service.State.TERMINATED;
import com.google.common.annotations.Beta;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.util.concurrent.Monitor.Guard;
import com.google.common.util.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 com.google.j2objc.annotations.WeakOuter;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.checkerframework.checker.nullness.qual.Nullable;
Base class for implementing services that can handle doStart
and doStop
requests, responding to them with notifyStarted()
and notifyStopped()
callbacks. Its subclasses must manage threads manually; consider AbstractExecutionThreadService
if you need only a single execution thread. Author: Jesse Wilson, Luke Sandberg Since: 1.0
/**
* Base class for implementing services that can handle {@link #doStart} and {@link #doStop}
* requests, responding to them with {@link #notifyStarted()} and {@link #notifyStopped()}
* callbacks. Its subclasses must manage threads manually; consider {@link
* AbstractExecutionThreadService} if you need only a single execution thread.
*
* @author Jesse Wilson
* @author Luke Sandberg
* @since 1.0
*/
@Beta
@GwtIncompatible
public abstract class AbstractService implements Service {
private static final ListenerCallQueue.Event<Listener> STARTING_EVENT =
new ListenerCallQueue.Event<Listener>() {
@Override
public void call(Listener listener) {
listener.starting();
}
@Override
public String toString() {
return "starting()";
}
};
private static final ListenerCallQueue.Event<Listener> RUNNING_EVENT =
new ListenerCallQueue.Event<Listener>() {
@Override
public void call(Listener listener) {
listener.running();
}
@Override
public String toString() {
return "running()";
}
};
private static final ListenerCallQueue.Event<Listener> STOPPING_FROM_STARTING_EVENT =
stoppingEvent(STARTING);
private static final ListenerCallQueue.Event<Listener> STOPPING_FROM_RUNNING_EVENT =
stoppingEvent(RUNNING);
private static final ListenerCallQueue.Event<Listener> TERMINATED_FROM_NEW_EVENT =
terminatedEvent(NEW);
private static final ListenerCallQueue.Event<Listener> TERMINATED_FROM_RUNNING_EVENT =
terminatedEvent(RUNNING);
private static final ListenerCallQueue.Event<Listener> TERMINATED_FROM_STOPPING_EVENT =
terminatedEvent(STOPPING);
private static ListenerCallQueue.Event<Listener> terminatedEvent(final State from) {
return new ListenerCallQueue.Event<Listener>() {
@Override
public void call(Listener listener) {
listener.terminated(from);
}
@Override
public String toString() {
return "terminated({from = " + from + "})";
}
};
}
private static ListenerCallQueue.Event<Listener> stoppingEvent(final State from) {
return new ListenerCallQueue.Event<Listener>() {
@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();
@WeakOuter
private final class IsStartableGuard extends Guard {
IsStartableGuard() {
super(AbstractService.this.monitor);
}
@Override
public boolean isSatisfied() {
return state() == NEW;
}
}
private final Guard isStoppable = new IsStoppableGuard();
@WeakOuter
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();
@WeakOuter
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();
@WeakOuter
private final class IsStoppedGuard extends Guard {
IsStoppedGuard() {
super(AbstractService.this.monitor);
}
@Override
public boolean isSatisfied() {
return state().isTerminal();
}
}
The listeners to notify during a state transition. /** The listeners to notify during a state transition. */
private final ListenerCallQueue<Listener> 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 state
, failureCause
and notably 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.
/**
* 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 {@link #state}, {@link #failureCause} and notably {@link #toString} can be run
* without grabbing the lock.
*
* <p>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. /** Constructor for use by subclasses. */
protected AbstractService() {}
This method is called by startAsync
to initiate service startup. The invocation of this method should cause a call to notifyStarted()
, either during this method's run, or after it has returned. If startup fails, the invocation should cause a call to 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 startAsync
is called multiple times.
/**
* This method is called by {@link #startAsync} to initiate service startup. The invocation of
* this method should cause a call to {@link #notifyStarted()}, either during this method's run,
* or after it has returned. If startup fails, the invocation should cause a call to {@link
* #notifyFailed(Throwable)} instead.
*
* <p>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 {@link #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 notifyStopped()
, either during this method's run, or after it has returned. If shutdown fails, the invocation should cause a call to 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 stopAsync
is called multiple times.
/**
* This method should be used to initiate service shutdown. The invocation of this method should
* cause a call to {@link #notifyStopped()}, either during this method's run, or after it has
* returned. If shutdown fails, the invocation should cause a call to {@link
* #notifyFailed(Throwable)} instead.
*
* <p>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 {@link #stopAsync} is
* called multiple times.
*/
@ForOverride
protected abstract void doStop();
@CanIgnoreReturnValue
@Override
public final Service startAsync() {
if (monitor.enterIf(isStartable)) {
try {
snapshot = new StateSnapshot(STARTING);
enqueueStartingEvent();
doStart();
} catch (Throwable 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);
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);
default:
throw new AssertionError("Unexpected state: " + previous);
}
} catch (Throwable shutdownFailure) {
notifyFailed(shutdownFailure);
} finally {
monitor.leave();
dispatchListenerEvents();
}
}
return this;
}
@Override
public final void awaitRunning() {
monitor.enterWhenUninterruptibly(hasReachedRunning);
try {
checkCurrentState(RUNNING);
} finally {
monitor.leave();
}
}
@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 the 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();
}
}
@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 the 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. /** 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 State.STARTING
to State.RUNNING
. Throws: - IllegalStateException – if the service is not
State.STARTING
.
/**
* Implementing classes should invoke this method once their service has started. It will cause
* the service to transition from {@link State#STARTING} to {@link State#RUNNING}.
*
* @throws IllegalStateException if the service is not {@link 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 State.STOPPING
to State.TERMINATED
. Throws: - IllegalStateException – if the service is neither
State.STOPPING
nor State.RUNNING
.
/**
* Implementing classes should invoke this method once their service has stopped. It will cause
* the service to transition from {@link State#STOPPING} to {@link State#TERMINATED}.
*
* @throws IllegalStateException if the service is neither {@link State#STOPPING} nor {@link
* State#RUNNING}.
*/
protected final void notifyStopped() {
monitor.enter();
try {
// We check the internal state of the snapshot instead of state() directly so we don't allow
// notifyStopped() to be called while STARTING, even if stop() has already been called.
State previous = snapshot.state;
if (previous != STOPPING && previous != RUNNING) {
IllegalStateException failure =
new IllegalStateException("Cannot notifyStopped() when the service is " + previous);
notifyFailed(failure);
throw failure;
}
snapshot = new StateSnapshot(TERMINATED);
enqueueTerminatedEvent(previous);
} finally {
monitor.leave();
dispatchListenerEvents();
}
}
Invoke this method to transition the service to the 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.
/**
* Invoke this method to transition the service to the {@link State#FAILED}. The service will
* <b>not be stopped</b> 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;
default:
throw new AssertionError("Unexpected state: " + previous);
}
} finally {
monitor.leave();
dispatchListenerEvents();
}
}
@Override
public final boolean isRunning() {
return state() == RUNNING;
}
@Override
public final State state() {
return snapshot.externalState();
}
Since: 14.0
/** @since 14.0 */
@Override
public final Throwable failureCause() {
return snapshot.failureCause();
}
Since: 13.0
/** @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 {@link #listeners} while not holding the {@link
* #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 RUNNING:
listeners.enqueue(TERMINATED_FROM_RUNNING_EVENT);
break;
case STOPPING:
listeners.enqueue(TERMINATED_FROM_STOPPING_EVENT);
break;
case STARTING:
case TERMINATED:
case FAILED:
default:
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<Listener>() {
@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.
/**
* 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.
/**
* 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. /** 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 null
unless the service has failed. /**
* The exception that caused this service to fail. This will be {@code null} unless the service
* has failed.
*/
final @Nullable Throwable failure;
StateSnapshot(State internalState) {
this(internalState, false, null);
}
StateSnapshot(
State internalState, boolean shutdownWhenStartupFinishes, @Nullable 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 Also:
/** @see Service#state() */
State externalState() {
if (shutdownWhenStartupFinishes && state == STARTING) {
return STOPPING;
} else {
return state;
}
}
See Also:
/** @see Service#failureCause() */
Throwable failureCause() {
checkState(
state == FAILED,
"failureCause() is only valid if the service has failed, service is %s",
state);
return failure;
}
}
}