Reworked state machine
parent
a9e2b1ddd9
commit
d037ada8c5
@ -0,0 +1,123 @@
|
|||||||
|
package eu.jonahbauer.wizard.common.machine;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public abstract class Context<S extends State<S,C>, C extends Context<S,C>> {
|
||||||
|
@Getter
|
||||||
|
private @NotNull S state;
|
||||||
|
private final ReentrantLock lock = new ReentrantLock();
|
||||||
|
|
||||||
|
public Context(@NotNull S initialState) {
|
||||||
|
this.state = initialState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomically executes the given supplier and transitions to the returned state if present.
|
||||||
|
* @see #transition(State)
|
||||||
|
*/
|
||||||
|
public void execute(Supplier<Optional<@NotNull S>> transition) {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
Optional<S> next = transition.get();
|
||||||
|
next.ifPresent(this::transition);
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomically applies the given function to the current state and transitions to the returned state if present.
|
||||||
|
* @see #transition(State)
|
||||||
|
*/
|
||||||
|
public void execute(Function<@NotNull S, Optional<@NotNull S>> transition) {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
Optional<S> next = transition.apply(state);
|
||||||
|
next.ifPresent(this::transition);
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomically transitions to the specified state calling the state lifecycle methods.
|
||||||
|
* When an error occurs during execution of the lifecycle methods {@link #handleError(Throwable)} is called.
|
||||||
|
* @param state the next state
|
||||||
|
* @see State#onExit(Context)
|
||||||
|
* @see State#onEnter(Context)
|
||||||
|
* @see #handleError(Throwable)
|
||||||
|
*/
|
||||||
|
public void transition(@NotNull S state) {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
//noinspection unchecked
|
||||||
|
this.state.onExit((C) this);
|
||||||
|
onTransition(this.state, state);
|
||||||
|
this.state = state;
|
||||||
|
//noinspection unchecked
|
||||||
|
state.onEnter((C) this).ifPresent(this::transition);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
handleError(t);
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomically checks that the current state is {@linkplain Object#equals(Object) equal to} the specified expected
|
||||||
|
* state and transitions to the specified next state.
|
||||||
|
* @param expected the expected current state
|
||||||
|
* @param next the next state
|
||||||
|
* @see #transition(State)
|
||||||
|
* @throws IllegalStateException if the current state is not equal to the expected state
|
||||||
|
*/
|
||||||
|
public void transition(@NotNull S expected, @NotNull S next) {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
if (Objects.equals(expected, this.state)) {
|
||||||
|
transition(next);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Current state is not " + expected + ".");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomically transitions to the specified state without calling the state lifecycle methods. This method can
|
||||||
|
* be used to recover from errors occurring during {@link State#onExit(Context)} which would otherwise prevent
|
||||||
|
* the context from transitioning to another state.
|
||||||
|
* @param state the next state
|
||||||
|
*/
|
||||||
|
protected void forceTransition(@NotNull S state) {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
onTransition(this.state, state);
|
||||||
|
this.state = state;
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method that will synchronously be called on transitioning between two states.
|
||||||
|
* @param from the previous state
|
||||||
|
* @param to the next state
|
||||||
|
*/
|
||||||
|
protected void onTransition(S from, S to) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method that will synchronously be called when an error occurs during execution of state lifecycle
|
||||||
|
* methods.
|
||||||
|
* @param t the cause
|
||||||
|
*/
|
||||||
|
protected abstract void handleError(Throwable t);
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package eu.jonahbauer.wizard.common.machine;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface State<S extends State<S,C>, C extends Context<S,C>> {
|
||||||
|
default Optional<S> onEnter(C context) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
default void onExit(C context) {}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package eu.jonahbauer.wizard.common.machine;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public abstract class TimeoutContext<S extends TimeoutState<S,C>, C extends TimeoutContext<S,C>> extends Context<S,C> {
|
||||||
|
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
|
||||||
|
public TimeoutContext(@NotNull S initialState) {
|
||||||
|
super(initialState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void timeout(@NotNull S currentState, long delay) {
|
||||||
|
scheduler.schedule(() -> {
|
||||||
|
if (Objects.equals(getState(), currentState)) {
|
||||||
|
execute(() -> {
|
||||||
|
if (Objects.equals(getState(), currentState)) {
|
||||||
|
//noinspection unchecked
|
||||||
|
return currentState.onTimeout((C) this);
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, delay, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdownNow() {
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package eu.jonahbauer.wizard.common.machine;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface TimeoutState<S extends TimeoutState<S,C>, C extends TimeoutContext<S,C>> extends State<S,C> {
|
||||||
|
default Optional<S> onTimeout(C context) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
@ -1,155 +0,0 @@
|
|||||||
package eu.jonahbauer.wizard.core.machine;
|
|
||||||
|
|
||||||
import eu.jonahbauer.wizard.core.machine.states.State;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.jetbrains.annotations.Blocking;
|
|
||||||
import org.jetbrains.annotations.NonBlocking;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.concurrent.*;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
public abstract class Context<S extends State<S,C>, C extends Context<S,C>> {
|
|
||||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
|
||||||
private final ThreadPoolExecutor executor = new ThreadPoolExecutor(
|
|
||||||
1, 1,
|
|
||||||
0, TimeUnit.SECONDS,
|
|
||||||
new PriorityBlockingQueue<>(
|
|
||||||
11,
|
|
||||||
Comparator.comparingInt(r -> r instanceof PriorityRunnable prio ? prio.getPriority() : Integer.MAX_VALUE)
|
|
||||||
.thenComparingLong(r -> r instanceof PriorityRunnable prio ? prio.getTimestamp() : Long.MAX_VALUE)
|
|
||||||
),
|
|
||||||
r -> {
|
|
||||||
var t = new Thread(r);
|
|
||||||
t.setUncaughtExceptionHandler((t1, e) -> finish(e));
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
protected S state;
|
|
||||||
|
|
||||||
private final CompletableFuture<Void> future = new CompletableFuture<>();
|
|
||||||
private final CompletableFuture<Void> finished = future.whenComplete((v, t) -> {
|
|
||||||
executor.shutdownNow();
|
|
||||||
scheduler.shutdownNow();
|
|
||||||
});
|
|
||||||
|
|
||||||
@NonBlocking
|
|
||||||
protected CompletableFuture<Void> submit(Runnable runnable) {
|
|
||||||
var future = new CompletableFuture<Void>();
|
|
||||||
executor.execute(new PriorityRunnable(100, () -> {
|
|
||||||
try {
|
|
||||||
runnable.run();
|
|
||||||
future.complete(null);
|
|
||||||
} catch (Throwable t) {
|
|
||||||
future.completeExceptionally(t);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
return future;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Blocking
|
|
||||||
protected void start(@NotNull S state) {
|
|
||||||
CountDownLatch latch = new CountDownLatch(1);
|
|
||||||
AtomicReference<RuntimeException> exception = new AtomicReference<>();
|
|
||||||
|
|
||||||
executor.execute(new PriorityRunnable(0, () -> {
|
|
||||||
if (future.isDone()) {
|
|
||||||
exception.set(new IllegalStateException("Context has already finished."));
|
|
||||||
latch.countDown();
|
|
||||||
} else {
|
|
||||||
latch.countDown();
|
|
||||||
doTransition(null, state);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
latch.await();
|
|
||||||
if (exception.get() != null) {
|
|
||||||
throw exception.get();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
} catch (InterruptedException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonBlocking
|
|
||||||
public void transition(S currentState, S newState) {
|
|
||||||
executor.execute(new PriorityRunnable(0, () -> doTransition(currentState, newState)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonBlocking
|
|
||||||
public void finish() {
|
|
||||||
finish(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonBlocking
|
|
||||||
public void finish(Throwable exception) {
|
|
||||||
executor.execute(new PriorityRunnable(0, () -> doFinish(exception)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonBlocking
|
|
||||||
public void cancel() {
|
|
||||||
finish(new CancellationException());
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* internal methods that are called on the executor
|
|
||||||
*/
|
|
||||||
|
|
||||||
private void doTransition(S currentState, S newState) {
|
|
||||||
if (state == currentState) {
|
|
||||||
state = newState;
|
|
||||||
if (currentState != null) //noinspection unchecked
|
|
||||||
currentState.onExit((C) this);
|
|
||||||
onTransition(currentState, newState);
|
|
||||||
if (newState != null) //noinspection unchecked
|
|
||||||
newState.onEnter((C) this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doFinish(Throwable t) {
|
|
||||||
if (future.isDone()) return;
|
|
||||||
|
|
||||||
doTransition(state, null);
|
|
||||||
if (t != null) {
|
|
||||||
future.completeExceptionally(t);
|
|
||||||
} else {
|
|
||||||
future.complete(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onTransition(S from, S to) {}
|
|
||||||
|
|
||||||
@Blocking
|
|
||||||
public void await() throws InterruptedException, ExecutionException, CancellationException {
|
|
||||||
finished.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void timeout(@NotNull S currentState, long delay) {
|
|
||||||
scheduler.schedule(() -> {
|
|
||||||
submit(() -> {
|
|
||||||
if (state == currentState) {
|
|
||||||
//noinspection unchecked
|
|
||||||
state.onTimeout((C) this);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, delay, TimeUnit.MILLISECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
private static class PriorityRunnable implements Runnable {
|
|
||||||
private final int priority;
|
|
||||||
private final long timestamp = System.nanoTime();
|
|
||||||
private final Runnable runnable;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
runnable.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
package eu.jonahbauer.wizard.core.machine.states;
|
|
||||||
|
|
||||||
import eu.jonahbauer.wizard.core.machine.Context;
|
|
||||||
|
|
||||||
public interface State<S extends State<S,C>, C extends Context<S,C>> {
|
|
||||||
default void onEnter(C context) {}
|
|
||||||
default void onTimeout(C context) {}
|
|
||||||
default void onExit(C context) {}
|
|
||||||
}
|
|
@ -0,0 +1,25 @@
|
|||||||
|
package eu.jonahbauer.wizard.core.machine.states.game;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.core.machine.Game;
|
||||||
|
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||||
|
import eu.jonahbauer.wizard.core.machine.GameState;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class Created extends GameState {
|
||||||
|
public Created() {
|
||||||
|
super(GameData.EMPTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<GameState> start(Game game, List<UUID> players) {
|
||||||
|
return transition(new Starting(GameData.EMPTY.with(GameData.PLAYERS, players)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
return obj instanceof Created;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package eu.jonahbauer.wizard.core.machine.states.game;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.core.machine.GameState;
|
||||||
|
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public final class Error extends GameState {
|
||||||
|
private final Throwable cause;
|
||||||
|
|
||||||
|
public Error(Throwable cause) {
|
||||||
|
super(GameData.EMPTY);
|
||||||
|
this.cause = cause;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package eu.jonahbauer.wizard.core.machine.states.game;
|
||||||
|
|
||||||
|
import eu.jonahbauer.wizard.core.machine.Game;
|
||||||
|
import eu.jonahbauer.wizard.core.machine.states.GameData;
|
||||||
|
import eu.jonahbauer.wizard.core.machine.GameState;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class Finished extends GameState {
|
||||||
|
public Finished() {
|
||||||
|
super(GameData.EMPTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<GameState> onEnter(Game context) {
|
||||||
|
context.complete(null);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue