修复异步状态发布一致性
This commit is contained in:
@@ -7,6 +7,7 @@ import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
@@ -18,11 +19,18 @@ public final class AsyncOperationController<T> {
|
||||
private final Object transitionLock = new Object();
|
||||
private final OperationStateMachine stateMachine = new OperationStateMachine();
|
||||
private final AtomicBoolean cancellationRequested = new AtomicBoolean();
|
||||
private final ConcurrentLinkedQueue<Publication> publicationQueue =
|
||||
new ConcurrentLinkedQueue<>();
|
||||
private final AtomicBoolean drainingPublications = new AtomicBoolean();
|
||||
private final ReadOnlyObjectWrapper<OperationSnapshot> snapshot =
|
||||
new ReadOnlyObjectWrapper<>(OperationSnapshot.idle());
|
||||
private final ReadOnlyBooleanWrapper running = new ReadOnlyBooleanWrapper(false);
|
||||
private boolean operationRunning;
|
||||
private long nextGeneration;
|
||||
private long activeGeneration;
|
||||
private long nextPublicationSequence;
|
||||
private long appliedGeneration;
|
||||
private long appliedPublicationSequence;
|
||||
|
||||
public AsyncOperationController(Executor executor, Consumer<Runnable> uiDispatcher) {
|
||||
this.executor = Objects.requireNonNull(executor, "executor");
|
||||
@@ -36,42 +44,46 @@ public final class AsyncOperationController<T> {
|
||||
public void start(String runningMessage, OperationTask<T> task,
|
||||
Consumer<T> onSuccess, Consumer<Throwable> onFailure) {
|
||||
Objects.requireNonNull(task, "task");
|
||||
long generation;
|
||||
StartTransition startTransition;
|
||||
synchronized (transitionLock) {
|
||||
if (running.get()) {
|
||||
if (operationRunning) {
|
||||
throw new IllegalStateException("Operation is already running");
|
||||
}
|
||||
generation = prepareStart(runningMessage);
|
||||
startTransition = prepareStart(runningMessage);
|
||||
}
|
||||
dispatchPublication(startTransition.publication());
|
||||
|
||||
executor.execute(() -> {
|
||||
OperationContext context = new OperationContext(
|
||||
(message, progress) -> dispatchProgress(
|
||||
generation, message, progress),
|
||||
startTransition.generation(), message, progress),
|
||||
cancellationRequested::get);
|
||||
try {
|
||||
T result = task.execute(context);
|
||||
uiDispatcher.accept(() -> completeSuccess(
|
||||
generation, result, onSuccess));
|
||||
startTransition.generation(), result, onSuccess));
|
||||
} catch (Throwable error) {
|
||||
if (error instanceof InterruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
uiDispatcher.accept(() -> completeFailure(
|
||||
generation, error, onFailure));
|
||||
startTransition.generation(), error, onFailure));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
Publication publication;
|
||||
synchronized (transitionLock) {
|
||||
if (!running.get()
|
||||
if (!operationRunning
|
||||
|| stateMachine.snapshot().state() != OperationState.RUNNING) {
|
||||
return;
|
||||
}
|
||||
cancellationRequested.set(true);
|
||||
publish(stateMachine.requestCancel());
|
||||
OperationSnapshot cancelling = stateMachine.requestCancel();
|
||||
publication = publication(cancelling);
|
||||
}
|
||||
dispatchPublication(publication);
|
||||
}
|
||||
|
||||
public OperationSnapshot snapshot() {
|
||||
@@ -86,52 +98,61 @@ public final class AsyncOperationController<T> {
|
||||
return running.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
private long prepareStart(String message) {
|
||||
private StartTransition prepareStart(String message) {
|
||||
OperationState current = stateMachine.snapshot().state();
|
||||
if (current == OperationState.SUCCEEDED || current == OperationState.FAILED
|
||||
|| current == OperationState.CANCELLED) {
|
||||
stateMachine.reset();
|
||||
}
|
||||
cancellationRequested.set(false);
|
||||
publish(stateMachine.start(message));
|
||||
running.set(true);
|
||||
OperationSnapshot started = stateMachine.start(message);
|
||||
operationRunning = true;
|
||||
activeGeneration = ++nextGeneration;
|
||||
return activeGeneration;
|
||||
return new StartTransition(activeGeneration, publication(started));
|
||||
}
|
||||
|
||||
private void dispatchProgress(long generation, String message, double progress) {
|
||||
uiDispatcher.accept(() -> {
|
||||
synchronized (transitionLock) {
|
||||
if (generation != activeGeneration
|
||||
|| stateMachine.snapshot().state() != OperationState.RUNNING) {
|
||||
return;
|
||||
}
|
||||
publish(stateMachine.progress(message, progress));
|
||||
uiDispatcher.accept(() -> applyProgress(generation, message, progress));
|
||||
}
|
||||
|
||||
private void applyProgress(long generation, String message, double progress) {
|
||||
Publication publication;
|
||||
synchronized (transitionLock) {
|
||||
if (generation != activeGeneration
|
||||
|| stateMachine.snapshot().state() != OperationState.RUNNING) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
publication = publication(stateMachine.progress(message, progress));
|
||||
}
|
||||
enqueuePublication(publication);
|
||||
}
|
||||
|
||||
private void completeSuccess(long generation, T result, Consumer<T> onSuccess) {
|
||||
Publication publication;
|
||||
synchronized (transitionLock) {
|
||||
if (generation != activeGeneration) {
|
||||
return;
|
||||
}
|
||||
OperationState state = stateMachine.snapshot().state();
|
||||
OperationSnapshot terminal;
|
||||
if (state == OperationState.RUNNING) {
|
||||
publish(stateMachine.succeed("操作完成"));
|
||||
terminal = stateMachine.succeed("操作完成");
|
||||
} else if (state == OperationState.CANCELLING) {
|
||||
publish(stateMachine.cancel());
|
||||
terminal = stateMachine.cancel();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
running.set(false);
|
||||
operationRunning = false;
|
||||
publication = publication(terminal);
|
||||
}
|
||||
enqueuePublication(publication);
|
||||
onSuccess.accept(result);
|
||||
}
|
||||
|
||||
private void completeFailure(long generation, Throwable error,
|
||||
Consumer<Throwable> onFailure) {
|
||||
boolean cancelled;
|
||||
Publication publication;
|
||||
synchronized (transitionLock) {
|
||||
if (generation != activeGeneration) {
|
||||
return;
|
||||
@@ -145,18 +166,58 @@ public final class AsyncOperationController<T> {
|
||||
&& cancellationRequested.get()
|
||||
&& (error instanceof CancellationException
|
||||
|| error instanceof InterruptedException);
|
||||
publish(cancelled
|
||||
OperationSnapshot terminal = cancelled
|
||||
? stateMachine.cancel()
|
||||
: stateMachine.fail(readableMessage(error)));
|
||||
running.set(false);
|
||||
: stateMachine.fail(readableMessage(error));
|
||||
operationRunning = false;
|
||||
publication = publication(terminal);
|
||||
}
|
||||
enqueuePublication(publication);
|
||||
if (!cancelled) {
|
||||
onFailure.accept(error);
|
||||
}
|
||||
}
|
||||
|
||||
private void publish(OperationSnapshot value) {
|
||||
snapshot.set(value);
|
||||
private Publication publication(OperationSnapshot value) {
|
||||
return new Publication(activeGeneration, ++nextPublicationSequence,
|
||||
operationRunning, value);
|
||||
}
|
||||
|
||||
private void dispatchPublication(Publication publication) {
|
||||
uiDispatcher.accept(() -> enqueuePublication(publication));
|
||||
}
|
||||
|
||||
private void enqueuePublication(Publication publication) {
|
||||
publicationQueue.add(publication);
|
||||
drainPublications();
|
||||
}
|
||||
|
||||
private void drainPublications() {
|
||||
if (!drainingPublications.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Publication publication;
|
||||
while ((publication = publicationQueue.poll()) != null) {
|
||||
applyPublication(publication);
|
||||
}
|
||||
} finally {
|
||||
drainingPublications.set(false);
|
||||
if (!publicationQueue.isEmpty()) {
|
||||
drainPublications();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyPublication(Publication publication) {
|
||||
if (publication.sequence() <= appliedPublicationSequence
|
||||
|| publication.generation() < appliedGeneration) {
|
||||
return;
|
||||
}
|
||||
appliedGeneration = publication.generation();
|
||||
appliedPublicationSequence = publication.sequence();
|
||||
running.set(publication.running());
|
||||
snapshot.set(publication.snapshot());
|
||||
}
|
||||
|
||||
private String readableMessage(Throwable error) {
|
||||
@@ -165,4 +226,11 @@ public final class AsyncOperationController<T> {
|
||||
? error.getClass().getSimpleName()
|
||||
: message;
|
||||
}
|
||||
|
||||
private record StartTransition(long generation, Publication publication) {
|
||||
}
|
||||
|
||||
private record Publication(long generation, long sequence, boolean running,
|
||||
OperationSnapshot snapshot) {
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user