修复异步状态发布一致性
This commit is contained in:
@@ -7,6 +7,7 @@ import javafx.beans.property.ReadOnlyObjectWrapper;
|
|||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
@@ -18,11 +19,18 @@ public final class AsyncOperationController<T> {
|
|||||||
private final Object transitionLock = new Object();
|
private final Object transitionLock = new Object();
|
||||||
private final OperationStateMachine stateMachine = new OperationStateMachine();
|
private final OperationStateMachine stateMachine = new OperationStateMachine();
|
||||||
private final AtomicBoolean cancellationRequested = new AtomicBoolean();
|
private final AtomicBoolean cancellationRequested = new AtomicBoolean();
|
||||||
|
private final ConcurrentLinkedQueue<Publication> publicationQueue =
|
||||||
|
new ConcurrentLinkedQueue<>();
|
||||||
|
private final AtomicBoolean drainingPublications = new AtomicBoolean();
|
||||||
private final ReadOnlyObjectWrapper<OperationSnapshot> snapshot =
|
private final ReadOnlyObjectWrapper<OperationSnapshot> snapshot =
|
||||||
new ReadOnlyObjectWrapper<>(OperationSnapshot.idle());
|
new ReadOnlyObjectWrapper<>(OperationSnapshot.idle());
|
||||||
private final ReadOnlyBooleanWrapper running = new ReadOnlyBooleanWrapper(false);
|
private final ReadOnlyBooleanWrapper running = new ReadOnlyBooleanWrapper(false);
|
||||||
|
private boolean operationRunning;
|
||||||
private long nextGeneration;
|
private long nextGeneration;
|
||||||
private long activeGeneration;
|
private long activeGeneration;
|
||||||
|
private long nextPublicationSequence;
|
||||||
|
private long appliedGeneration;
|
||||||
|
private long appliedPublicationSequence;
|
||||||
|
|
||||||
public AsyncOperationController(Executor executor, Consumer<Runnable> uiDispatcher) {
|
public AsyncOperationController(Executor executor, Consumer<Runnable> uiDispatcher) {
|
||||||
this.executor = Objects.requireNonNull(executor, "executor");
|
this.executor = Objects.requireNonNull(executor, "executor");
|
||||||
@@ -36,42 +44,46 @@ public final class AsyncOperationController<T> {
|
|||||||
public void start(String runningMessage, OperationTask<T> task,
|
public void start(String runningMessage, OperationTask<T> task,
|
||||||
Consumer<T> onSuccess, Consumer<Throwable> onFailure) {
|
Consumer<T> onSuccess, Consumer<Throwable> onFailure) {
|
||||||
Objects.requireNonNull(task, "task");
|
Objects.requireNonNull(task, "task");
|
||||||
long generation;
|
StartTransition startTransition;
|
||||||
synchronized (transitionLock) {
|
synchronized (transitionLock) {
|
||||||
if (running.get()) {
|
if (operationRunning) {
|
||||||
throw new IllegalStateException("Operation is already running");
|
throw new IllegalStateException("Operation is already running");
|
||||||
}
|
}
|
||||||
generation = prepareStart(runningMessage);
|
startTransition = prepareStart(runningMessage);
|
||||||
}
|
}
|
||||||
|
dispatchPublication(startTransition.publication());
|
||||||
|
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
OperationContext context = new OperationContext(
|
OperationContext context = new OperationContext(
|
||||||
(message, progress) -> dispatchProgress(
|
(message, progress) -> dispatchProgress(
|
||||||
generation, message, progress),
|
startTransition.generation(), message, progress),
|
||||||
cancellationRequested::get);
|
cancellationRequested::get);
|
||||||
try {
|
try {
|
||||||
T result = task.execute(context);
|
T result = task.execute(context);
|
||||||
uiDispatcher.accept(() -> completeSuccess(
|
uiDispatcher.accept(() -> completeSuccess(
|
||||||
generation, result, onSuccess));
|
startTransition.generation(), result, onSuccess));
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
if (error instanceof InterruptedException) {
|
if (error instanceof InterruptedException) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
uiDispatcher.accept(() -> completeFailure(
|
uiDispatcher.accept(() -> completeFailure(
|
||||||
generation, error, onFailure));
|
startTransition.generation(), error, onFailure));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancel() {
|
public void cancel() {
|
||||||
|
Publication publication;
|
||||||
synchronized (transitionLock) {
|
synchronized (transitionLock) {
|
||||||
if (!running.get()
|
if (!operationRunning
|
||||||
|| stateMachine.snapshot().state() != OperationState.RUNNING) {
|
|| stateMachine.snapshot().state() != OperationState.RUNNING) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cancellationRequested.set(true);
|
cancellationRequested.set(true);
|
||||||
publish(stateMachine.requestCancel());
|
OperationSnapshot cancelling = stateMachine.requestCancel();
|
||||||
|
publication = publication(cancelling);
|
||||||
}
|
}
|
||||||
|
dispatchPublication(publication);
|
||||||
}
|
}
|
||||||
|
|
||||||
public OperationSnapshot snapshot() {
|
public OperationSnapshot snapshot() {
|
||||||
@@ -86,52 +98,61 @@ public final class AsyncOperationController<T> {
|
|||||||
return running.getReadOnlyProperty();
|
return running.getReadOnlyProperty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private long prepareStart(String message) {
|
private StartTransition prepareStart(String message) {
|
||||||
OperationState current = stateMachine.snapshot().state();
|
OperationState current = stateMachine.snapshot().state();
|
||||||
if (current == OperationState.SUCCEEDED || current == OperationState.FAILED
|
if (current == OperationState.SUCCEEDED || current == OperationState.FAILED
|
||||||
|| current == OperationState.CANCELLED) {
|
|| current == OperationState.CANCELLED) {
|
||||||
stateMachine.reset();
|
stateMachine.reset();
|
||||||
}
|
}
|
||||||
cancellationRequested.set(false);
|
cancellationRequested.set(false);
|
||||||
publish(stateMachine.start(message));
|
OperationSnapshot started = stateMachine.start(message);
|
||||||
running.set(true);
|
operationRunning = true;
|
||||||
activeGeneration = ++nextGeneration;
|
activeGeneration = ++nextGeneration;
|
||||||
return activeGeneration;
|
return new StartTransition(activeGeneration, publication(started));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void dispatchProgress(long generation, String message, double progress) {
|
private void dispatchProgress(long generation, String message, double progress) {
|
||||||
uiDispatcher.accept(() -> {
|
uiDispatcher.accept(() -> applyProgress(generation, message, progress));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyProgress(long generation, String message, double progress) {
|
||||||
|
Publication publication;
|
||||||
synchronized (transitionLock) {
|
synchronized (transitionLock) {
|
||||||
if (generation != activeGeneration
|
if (generation != activeGeneration
|
||||||
|| stateMachine.snapshot().state() != OperationState.RUNNING) {
|
|| stateMachine.snapshot().state() != OperationState.RUNNING) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
publish(stateMachine.progress(message, progress));
|
publication = publication(stateMachine.progress(message, progress));
|
||||||
}
|
}
|
||||||
});
|
enqueuePublication(publication);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void completeSuccess(long generation, T result, Consumer<T> onSuccess) {
|
private void completeSuccess(long generation, T result, Consumer<T> onSuccess) {
|
||||||
|
Publication publication;
|
||||||
synchronized (transitionLock) {
|
synchronized (transitionLock) {
|
||||||
if (generation != activeGeneration) {
|
if (generation != activeGeneration) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
OperationState state = stateMachine.snapshot().state();
|
OperationState state = stateMachine.snapshot().state();
|
||||||
|
OperationSnapshot terminal;
|
||||||
if (state == OperationState.RUNNING) {
|
if (state == OperationState.RUNNING) {
|
||||||
publish(stateMachine.succeed("操作完成"));
|
terminal = stateMachine.succeed("操作完成");
|
||||||
} else if (state == OperationState.CANCELLING) {
|
} else if (state == OperationState.CANCELLING) {
|
||||||
publish(stateMachine.cancel());
|
terminal = stateMachine.cancel();
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
running.set(false);
|
operationRunning = false;
|
||||||
|
publication = publication(terminal);
|
||||||
}
|
}
|
||||||
|
enqueuePublication(publication);
|
||||||
onSuccess.accept(result);
|
onSuccess.accept(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void completeFailure(long generation, Throwable error,
|
private void completeFailure(long generation, Throwable error,
|
||||||
Consumer<Throwable> onFailure) {
|
Consumer<Throwable> onFailure) {
|
||||||
boolean cancelled;
|
boolean cancelled;
|
||||||
|
Publication publication;
|
||||||
synchronized (transitionLock) {
|
synchronized (transitionLock) {
|
||||||
if (generation != activeGeneration) {
|
if (generation != activeGeneration) {
|
||||||
return;
|
return;
|
||||||
@@ -145,18 +166,58 @@ public final class AsyncOperationController<T> {
|
|||||||
&& cancellationRequested.get()
|
&& cancellationRequested.get()
|
||||||
&& (error instanceof CancellationException
|
&& (error instanceof CancellationException
|
||||||
|| error instanceof InterruptedException);
|
|| error instanceof InterruptedException);
|
||||||
publish(cancelled
|
OperationSnapshot terminal = cancelled
|
||||||
? stateMachine.cancel()
|
? stateMachine.cancel()
|
||||||
: stateMachine.fail(readableMessage(error)));
|
: stateMachine.fail(readableMessage(error));
|
||||||
running.set(false);
|
operationRunning = false;
|
||||||
|
publication = publication(terminal);
|
||||||
}
|
}
|
||||||
|
enqueuePublication(publication);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
onFailure.accept(error);
|
onFailure.accept(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void publish(OperationSnapshot value) {
|
private Publication publication(OperationSnapshot value) {
|
||||||
snapshot.set(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) {
|
private String readableMessage(Throwable error) {
|
||||||
@@ -165,4 +226,11 @@ public final class AsyncOperationController<T> {
|
|||||||
? error.getClass().getSimpleName()
|
? error.getClass().getSimpleName()
|
||||||
: message;
|
: message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record StartTransition(long generation, Publication publication) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Publication(long generation, long sequence, boolean running,
|
||||||
|
OperationSnapshot snapshot) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class SharedPaneAccessibilityTest {
|
|||||||
}, ignored -> { });
|
}, ignored -> { });
|
||||||
pendingOperation.get().run();
|
pendingOperation.get().run();
|
||||||
uiUpdates.get(0).run();
|
uiUpdates.get(0).run();
|
||||||
|
uiUpdates.get(1).run();
|
||||||
|
|
||||||
assertTrue(pane.getChildren().stream()
|
assertTrue(pane.getChildren().stream()
|
||||||
.filter(javafx.scene.control.Label.class::isInstance)
|
.filter(javafx.scene.control.Label.class::isInstance)
|
||||||
@@ -102,6 +103,7 @@ class SharedPaneAccessibilityTest {
|
|||||||
.anyMatch(text -> text.contains("42%")));
|
.anyMatch(text -> text.contains("42%")));
|
||||||
|
|
||||||
cancelButton.fire();
|
cancelButton.fire();
|
||||||
|
uiUpdates.get(3).run();
|
||||||
assertEquals(OperationState.CANCELLING, controller.snapshot().state());
|
assertEquals(OperationState.CANCELLING, controller.snapshot().state());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+174
-1
@@ -222,6 +222,12 @@ class AsyncOperationControllerTest {
|
|||||||
}, ignored -> { });
|
}, ignored -> { });
|
||||||
controller.cancel();
|
controller.cancel();
|
||||||
|
|
||||||
|
ui.runLast();
|
||||||
|
assertEquals(OperationState.CANCELLING, controller.snapshot().state());
|
||||||
|
|
||||||
|
ui.runFirst();
|
||||||
|
assertEquals(OperationState.CANCELLING, controller.snapshot().state());
|
||||||
|
|
||||||
assertDoesNotThrow(ui::runFirst);
|
assertDoesNotThrow(ui::runFirst);
|
||||||
assertEquals(OperationState.CANCELLING, controller.snapshot().state());
|
assertEquals(OperationState.CANCELLING, controller.snapshot().state());
|
||||||
|
|
||||||
@@ -243,28 +249,34 @@ class AsyncOperationControllerTest {
|
|||||||
ui.runLast();
|
ui.runLast();
|
||||||
assertEquals(OperationState.SUCCEEDED, controller.snapshot().state());
|
assertEquals(OperationState.SUCCEEDED, controller.snapshot().state());
|
||||||
assertDoesNotThrow(ui::runFirst);
|
assertDoesNotThrow(ui::runFirst);
|
||||||
|
assertDoesNotThrow(ui::runFirst);
|
||||||
assertEquals(OperationState.SUCCEEDED, controller.snapshot().state());
|
assertEquals(OperationState.SUCCEEDED, controller.snapshot().state());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void previousOperationProgressCannotPolluteRestartedOperation() {
|
void previousOperationProgressCannotPolluteRestartedOperation() {
|
||||||
QueuedUiDispatcher ui = new QueuedUiDispatcher();
|
QueuedUiDispatcher ui = new QueuedUiDispatcher();
|
||||||
|
Deque<Runnable> operations = new ArrayDeque<>();
|
||||||
AsyncOperationController<String> controller =
|
AsyncOperationController<String> controller =
|
||||||
new AsyncOperationController<>(Runnable::run, ui);
|
new AsyncOperationController<>(operations::addLast, ui);
|
||||||
|
|
||||||
controller.start("旧任务", context -> {
|
controller.start("旧任务", context -> {
|
||||||
context.report("旧进度", 0.75);
|
context.report("旧进度", 0.75);
|
||||||
return "old-result";
|
return "old-result";
|
||||||
}, ignored -> { });
|
}, ignored -> { });
|
||||||
|
operations.removeFirst().run();
|
||||||
ui.runLast();
|
ui.runLast();
|
||||||
controller.start("新任务", context -> "new-result", ignored -> { });
|
controller.start("新任务", context -> "new-result", ignored -> { });
|
||||||
|
ui.runLast();
|
||||||
|
|
||||||
|
ui.runFirst();
|
||||||
ui.runFirst();
|
ui.runFirst();
|
||||||
|
|
||||||
assertEquals(OperationState.RUNNING, controller.snapshot().state());
|
assertEquals(OperationState.RUNNING, controller.snapshot().state());
|
||||||
assertEquals("新任务", controller.snapshot().message());
|
assertEquals("新任务", controller.snapshot().message());
|
||||||
assertEquals(-1, controller.snapshot().progress());
|
assertEquals(-1, controller.snapshot().progress());
|
||||||
|
|
||||||
|
operations.removeFirst().run();
|
||||||
ui.runFirst();
|
ui.runFirst();
|
||||||
assertEquals(OperationState.SUCCEEDED, controller.snapshot().state());
|
assertEquals(OperationState.SUCCEEDED, controller.snapshot().state());
|
||||||
}
|
}
|
||||||
@@ -303,8 +315,10 @@ class AsyncOperationControllerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assertTrue(errors.isEmpty(), () -> "cancel errors: " + errors);
|
assertTrue(errors.isEmpty(), () -> "cancel errors: " + errors);
|
||||||
|
ui.runLast();
|
||||||
assertEquals(OperationState.CANCELLING, controller.snapshot().state());
|
assertEquals(OperationState.CANCELLING, controller.snapshot().state());
|
||||||
|
|
||||||
|
ui.runFirst();
|
||||||
ui.runFirst();
|
ui.runFirst();
|
||||||
assertEquals(OperationState.CANCELLED, controller.snapshot().state());
|
assertEquals(OperationState.CANCELLED, controller.snapshot().state());
|
||||||
assertFalse(controller.runningProperty().get());
|
assertFalse(controller.runningProperty().get());
|
||||||
@@ -318,6 +332,7 @@ class AsyncOperationControllerTest {
|
|||||||
AsyncOperationController<String> controller =
|
AsyncOperationController<String> controller =
|
||||||
new AsyncOperationController<>(Runnable::run, ui);
|
new AsyncOperationController<>(Runnable::run, ui);
|
||||||
controller.start("运行中", context -> "valid-result", result::set);
|
controller.start("运行中", context -> "valid-result", result::set);
|
||||||
|
ui.runFirst();
|
||||||
CountDownLatch ready = new CountDownLatch(2);
|
CountDownLatch ready = new CountDownLatch(2);
|
||||||
CountDownLatch release = new CountDownLatch(1);
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
List<Throwable> errors = java.util.Collections.synchronizedList(
|
List<Throwable> errors = java.util.Collections.synchronizedList(
|
||||||
@@ -344,6 +359,164 @@ class AsyncOperationControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void runningSnapshotListenerCanImmediatelyCancel() {
|
||||||
|
AtomicReference<Runnable> pendingOperation = new AtomicReference<>();
|
||||||
|
AtomicBoolean runningWasPublishedFirst = new AtomicBoolean();
|
||||||
|
AsyncOperationController<String> controller =
|
||||||
|
new AsyncOperationController<>(pendingOperation::set, Runnable::run);
|
||||||
|
controller.snapshotProperty().addListener((observable, oldValue, value) -> {
|
||||||
|
if (value.state() == OperationState.RUNNING) {
|
||||||
|
runningWasPublishedFirst.set(controller.runningProperty().get());
|
||||||
|
controller.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.start("运行中", context -> "result", ignored -> { });
|
||||||
|
|
||||||
|
assertTrue(runningWasPublishedFirst.get());
|
||||||
|
assertEquals(OperationState.CANCELLING, controller.snapshot().state());
|
||||||
|
pendingOperation.get().run();
|
||||||
|
assertEquals(OperationState.CANCELLED, controller.snapshot().state());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void terminalSnapshotListenerCanImmediatelyRestart() {
|
||||||
|
Deque<Runnable> operations = new ArrayDeque<>();
|
||||||
|
AtomicBoolean restarted = new AtomicBoolean();
|
||||||
|
AtomicBoolean terminalSawStopped = new AtomicBoolean();
|
||||||
|
AsyncOperationController<String> controller =
|
||||||
|
new AsyncOperationController<>(operations::addLast, Runnable::run);
|
||||||
|
controller.snapshotProperty().addListener((observable, oldValue, value) -> {
|
||||||
|
if (value.state() == OperationState.SUCCEEDED
|
||||||
|
&& !restarted.get()) {
|
||||||
|
terminalSawStopped.set(!controller.runningProperty().get());
|
||||||
|
controller.start("第二个任务", context -> "second", ignored -> { });
|
||||||
|
restarted.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.start("第一个任务", context -> "first", ignored -> { });
|
||||||
|
operations.removeFirst().run();
|
||||||
|
|
||||||
|
assertTrue(restarted.get());
|
||||||
|
assertTrue(terminalSawStopped.get());
|
||||||
|
assertTrue(controller.runningProperty().get());
|
||||||
|
assertEquals(OperationState.RUNNING, controller.snapshot().state());
|
||||||
|
assertEquals("第二个任务", controller.snapshot().message());
|
||||||
|
|
||||||
|
operations.removeFirst().run();
|
||||||
|
assertEquals(OperationState.SUCCEEDED, controller.snapshot().state());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void blockingSnapshotListenerDoesNotHoldTransitionLock() throws Exception {
|
||||||
|
AtomicReference<Runnable> pendingOperation = new AtomicReference<>();
|
||||||
|
AsyncOperationController<String> controller =
|
||||||
|
new AsyncOperationController<>(pendingOperation::set, Runnable::run);
|
||||||
|
CountDownLatch listenerEntered = new CountDownLatch(1);
|
||||||
|
CountDownLatch releaseListener = new CountDownLatch(1);
|
||||||
|
CountDownLatch cancelReturned = new CountDownLatch(1);
|
||||||
|
controller.snapshotProperty().addListener((observable, oldValue, value) -> {
|
||||||
|
if (value.state() == OperationState.RUNNING) {
|
||||||
|
listenerEntered.countDown();
|
||||||
|
try {
|
||||||
|
releaseListener.await();
|
||||||
|
} catch (InterruptedException error) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new AssertionError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Thread starter = new Thread(() -> controller.start(
|
||||||
|
"运行中", context -> "result", ignored -> { }));
|
||||||
|
Thread canceller = new Thread(() -> {
|
||||||
|
controller.cancel();
|
||||||
|
cancelReturned.countDown();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
starter.start();
|
||||||
|
assertTrue(listenerEntered.await(2, TimeUnit.SECONDS));
|
||||||
|
canceller.start();
|
||||||
|
|
||||||
|
assertTrue(cancelReturned.await(1, TimeUnit.SECONDS),
|
||||||
|
"a listener must not retain the transition lock");
|
||||||
|
} finally {
|
||||||
|
releaseListener.countDown();
|
||||||
|
starter.join(2_000);
|
||||||
|
canceller.join(2_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFalse(starter.isAlive());
|
||||||
|
assertFalse(canceller.isAlive());
|
||||||
|
assertEquals(OperationState.CANCELLING, controller.snapshot().state());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void backgroundCancelPublishesOnUiDispatcherThread() throws Exception {
|
||||||
|
ExecutorService uiExecutor = Executors.newSingleThreadExecutor(action -> {
|
||||||
|
Thread thread = new Thread(action);
|
||||||
|
thread.setName("operation-ui-dispatcher");
|
||||||
|
return thread;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
AtomicReference<Runnable> pendingOperation = new AtomicReference<>();
|
||||||
|
AsyncOperationController<String> controller =
|
||||||
|
new AsyncOperationController<>(pendingOperation::set, uiExecutor::execute);
|
||||||
|
CountDownLatch runningSeen = new CountDownLatch(1);
|
||||||
|
CountDownLatch cancellingSeen = new CountDownLatch(1);
|
||||||
|
AtomicReference<String> runningThread = new AtomicReference<>();
|
||||||
|
AtomicReference<String> cancellingThread = new AtomicReference<>();
|
||||||
|
controller.snapshotProperty().addListener((observable, oldValue, value) -> {
|
||||||
|
if (value.state() == OperationState.RUNNING) {
|
||||||
|
runningThread.set(Thread.currentThread().getName());
|
||||||
|
runningSeen.countDown();
|
||||||
|
} else if (value.state() == OperationState.CANCELLING) {
|
||||||
|
cancellingThread.set(Thread.currentThread().getName());
|
||||||
|
cancellingSeen.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.start("运行中", context -> "result", ignored -> { });
|
||||||
|
assertTrue(runningSeen.await(2, TimeUnit.SECONDS));
|
||||||
|
Thread background = new Thread(controller::cancel, "background-canceller");
|
||||||
|
background.start();
|
||||||
|
background.join(2_000);
|
||||||
|
|
||||||
|
assertFalse(background.isAlive());
|
||||||
|
assertTrue(cancellingSeen.await(2, TimeUnit.SECONDS));
|
||||||
|
assertEquals("operation-ui-dispatcher", runningThread.get());
|
||||||
|
assertEquals("operation-ui-dispatcher", cancellingThread.get());
|
||||||
|
} finally {
|
||||||
|
uiExecutor.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void outOfOrderPublicationsCannotRollBackVisibleState() {
|
||||||
|
QueuedUiDispatcher ui = new QueuedUiDispatcher();
|
||||||
|
AtomicReference<Runnable> pendingOperation = new AtomicReference<>();
|
||||||
|
AsyncOperationController<String> controller =
|
||||||
|
new AsyncOperationController<>(pendingOperation::set, ui);
|
||||||
|
|
||||||
|
controller.start("运行中", context -> "result", ignored -> { });
|
||||||
|
controller.cancel();
|
||||||
|
|
||||||
|
ui.runLast();
|
||||||
|
assertTrue(controller.runningProperty().get());
|
||||||
|
assertEquals(OperationState.CANCELLING, controller.snapshot().state());
|
||||||
|
|
||||||
|
ui.runFirst();
|
||||||
|
assertTrue(controller.runningProperty().get());
|
||||||
|
assertEquals(OperationState.CANCELLING, controller.snapshot().state());
|
||||||
|
|
||||||
|
pendingOperation.get().run();
|
||||||
|
ui.runFirst();
|
||||||
|
assertFalse(controller.runningProperty().get());
|
||||||
|
assertEquals(OperationState.CANCELLED, controller.snapshot().state());
|
||||||
|
}
|
||||||
|
|
||||||
private static void race(CountDownLatch ready, CountDownLatch release,
|
private static void race(CountDownLatch ready, CountDownLatch release,
|
||||||
Runnable action, List<Throwable> errors) {
|
Runnable action, List<Throwable> errors) {
|
||||||
ready.countDown();
|
ready.countDown();
|
||||||
|
|||||||
Reference in New Issue
Block a user