blob: 9870fe3719aae0aeb0a2483242aa75065f111b72 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.ratis.util;
import org.apache.ratis.util.function.CheckedRunnable;
import org.apache.ratis.util.function.CheckedSupplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.UnaryOperator;
/**
* The life cycle of a machine.
* <pre>
* -------------------------------------------------
* | -------------------------------- |
* | | ------------------------ | |
* | | | | | |
* | | PAUSED <---- PAUSING---- | | |
* | | | ^ | | | | |
* | | V | | V V V V
* NEW --> STARTING --> RUNNING --|--> CLOSING --> [CLOSED]
* ^ | | | | ^
* | | | V V |
* ------- -------> EXCEPTION -----
* </pre>
*/
public class LifeCycle {
public static final Logger LOG = LoggerFactory.getLogger(LifeCycle.class);
/** The states in the life cycle. */
public enum State {
/** The machine is newly created and holds zero resource. */
NEW,
/** The machine is starting and does not yet provide any service. */
STARTING,
/** The machine is running and providing service. */
RUNNING,
/** The machine is pausing and stopping providing service. */
PAUSING,
/** The machine is paused and does not provide any service. */
PAUSED,
/** The machine catches an internal exception so that it must be closed. */
EXCEPTION,
/** The machine is closing, stopping providing service and releasing resources. */
CLOSING,
/** The machine is closed, a final state. */
CLOSED;
private static final Map<State, List<State>> PREDECESSORS;
/** Is this {@link State#RUNNING}? */
public boolean isRunning() {
return this == RUNNING;
}
/** Is this {@link State#CLOSING} or {@link State#CLOSED}? */
public boolean isClosingOrClosed() {
return States.CLOSING_OR_CLOSED.contains(this);
}
/** Is this {@link State#PAUSING} or {@link State#PAUSED}? */
public boolean isPausingOrPaused() {
return States.PAUSING_OR_PAUSED.contains(this);
}
static void put(State key, Map<State, List<State>> map, State... values) {
map.put(key, Collections.unmodifiableList(Arrays.asList(values)));
}
static {
final Map<State, List<State>> predecessors = new EnumMap<>(State.class);
put(NEW, predecessors, STARTING);
put(STARTING, predecessors, NEW, PAUSED);
put(RUNNING, predecessors, STARTING);
put(PAUSING, predecessors, RUNNING);
put(PAUSED, predecessors, PAUSING);
put(EXCEPTION, predecessors, STARTING, PAUSING, RUNNING);
put(CLOSING, predecessors, STARTING, RUNNING, PAUSING, PAUSED, EXCEPTION);
put(CLOSED, predecessors, NEW, CLOSING);
PREDECESSORS = Collections.unmodifiableMap(predecessors);
}
/** Is the given transition valid? */
public static boolean isValid(State from, State to) {
return PREDECESSORS.get(to).contains(from);
}
/** Validate the given transition. */
static void validate(Object name, State from, State to) {
LOG.debug("{}: {} -> {}", name, from, to);
if (LOG.isTraceEnabled()) {
LOG.trace("TRACE", new Throwable());
}
Preconditions.assertTrue(isValid(from, to),
"ILLEGAL TRANSITION: In %s, %s -> %s", name, from, to);
}
}
public static final class States {
public static final Set<State> RUNNING
= Collections.unmodifiableSet(EnumSet.of(State.RUNNING));
public static final Set<State> STARTING_OR_RUNNING
= Collections.unmodifiableSet(EnumSet.of(State.STARTING, State.RUNNING));
public static final Set<State> CLOSING_OR_CLOSED
= Collections.unmodifiableSet(EnumSet.of(State.CLOSING, State.CLOSED));
public static final Set<State> PAUSING_OR_PAUSED
= Collections.unmodifiableSet(EnumSet.of(State.PAUSING, State.PAUSED));
public static final Set<State> CLOSING_OR_CLOSED_OR_EXCEPTION
= Collections.unmodifiableSet(EnumSet.of(State.CLOSING, State.CLOSED, State.EXCEPTION));
private States() {
// no instances
}
}
private volatile String name;
private final AtomicReference<State> current = new AtomicReference<>(State.NEW);
public LifeCycle(Object name) {
this.name = name.toString();
LOG.debug("{}: {}", name, current);
}
public void setName(String name) {
this.name = name;
}
/** Transition from the current state to the given state. */
public void transition(final State to) {
current.updateAndGet(from -> {
State.validate(name, from, to);
return to;
});
}
/** Transition from the current state to the given state if the current state is not equal to the given state. */
public void transitionIfNotEqual(final State to) {
current.updateAndGet(from -> {
if (from != to) {
State.validate(name, from, to);
}
return to;
});
}
/**
* Transition from the current state to the given state only if the transition is valid.
* If the transition is invalid, this is a no-op.
*
* @return true if the updated state equals to the given state.
*/
public boolean transitionIfValid(final State to) {
final State updated = current.updateAndGet(from -> State.isValid(from, to)? to : from);
return updated == to;
}
/**
* Transition using the given operator.
*
* @return the updated state if there is a transition;
* otherwise, return null to indicate no state change.
*/
public State transition(UnaryOperator<State> operator) {
for(;;) {
final State previous = current.get();
final State applied = operator.apply(previous);
if (previous == applied) {
return null; // no change required
}
State.validate(name, previous, applied);
if (current.compareAndSet(previous, applied)) {
return applied;
}
// state has been changed, retry
}
}
/**
* Transition using the given operator.
*
* @return the updated state.
*/
public State transitionAndGet(UnaryOperator<State> operator) {
return current.updateAndGet(previous -> {
final State applied = operator.apply(previous);
if (applied != previous) {
State.validate(name, previous, applied);
}
return applied;
});
}
/**
* If the current state is equal to the specified from state,
* then transition to the give to state; otherwise, make no change.
*
* @return true iff the current state is equal to the specified from state.
*/
public boolean compareAndTransition(final State from, final State to) {
final State previous = current.getAndUpdate(state -> {
if (state != from) {
return state;
}
State.validate(name, from, to);
return to;
});
return previous == from;
}
/** @return the current state. */
public State getCurrentState() {
return current.get();
}
/** Assert if the current state equals to one of the expected states. */
public void assertCurrentState(Set<State> expected) {
assertCurrentState((n, c) -> new IllegalStateException("STATE MISMATCHED: In "
+ n + ", current state " + c + " is not one of the expected states "
+ expected), expected);
}
/** Assert if the current state equals to one of the expected states. */
public <T extends Throwable> State assertCurrentState(
BiFunction<String, State, T> newThrowable, Set<State> expected) throws T {
final State c = getCurrentState();
if (!expected.contains(c)) {
throw newThrowable.apply(name, c);
}
return c;
}
@Override
public String toString() {
return name + ":" + getCurrentState();
}
/** Run the given start method and transition the current state accordingly. */
@SafeVarargs
public final <T extends Throwable> void startAndTransition(
CheckedRunnable<T> startImpl, Class<? extends Throwable>... exceptionClasses)
throws T {
transition(State.STARTING);
try {
startImpl.run();
transition(State.RUNNING);
} catch (Throwable t) {
transition(ReflectionUtils.isInstance(t, exceptionClasses)?
State.NEW: State.EXCEPTION);
throw t;
}
}
/**
* Check the current state and, if applicable, transit to {@link State#CLOSING}.
* If this is already in {@link State#CLOSING} or {@link State#CLOSED},
* then invoking this method has no effect.
* In other words, this method can be safely called multiple times.
*/
public State checkStateAndClose() {
return checkStateAndClose(() -> State.CLOSING);
}
/**
* Check the current state and, if applicable, run the given close method.
* If this is already in {@link State#CLOSING} or {@link State#CLOSED},
* then invoking this method has no effect.
* In other words, this method can be safely called multiple times
* while the given close method will only be executed at most once.
*/
public <T extends Throwable> State checkStateAndClose(CheckedRunnable<T> closeMethod) throws T {
return checkStateAndClose(() -> {
try {
closeMethod.run();
} finally {
transition(State.CLOSED);
}
return State.CLOSED;
});
}
private <T extends Throwable> State checkStateAndClose(CheckedSupplier<State, T> closeMethod) throws T {
if (compareAndTransition(State.NEW, State.CLOSED)) {
return State.CLOSED;
}
for(;;) {
final State c = getCurrentState();
if (c.isClosingOrClosed()) {
return c; //already closing or closed.
}
if (compareAndTransition(c, State.CLOSING)) {
return closeMethod.get();
}
// lifecycle state is changed, retry.
}
}
}