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