修复异步状态发布一致性

This commit is contained in:
张家训
2026-08-01 18:06:11 +08:00
parent 550dbd3b55
commit 9b26134e46
3 changed files with 272 additions and 29 deletions
@@ -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) {
}
}