Skip to content

Commit aa1ebc0

Browse files
committed
[Fix #1749] Alternative approach
Signed-off-by: Francisco Javier Tirado Sarti <ftirados@ibm.com>
1 parent 8085c7b commit aa1ebc0

3 files changed

Lines changed: 121 additions & 125 deletions

File tree

‎impl/core/src/main/java/io/serverlessworkflow/impl/LifecycleEventsUtils.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ public class LifecycleEventsUtils {
2828

2929
private LifecycleEventsUtils() {}
3030

31+
public static CompletableFuture<Boolean> publishEvent(
32+
boolean statusChanged,
33+
WorkflowContext workflowContext,
34+
Function<WorkflowExecutionCompletableListener, CompletableFuture<?>> function) {
35+
return statusChanged
36+
? publishEvent(workflowContext, function).thenApply(__ -> true)
37+
: CompletableFuture.completedFuture(false);
38+
}
39+
3140
public static CompletableFuture<?> publishEvent(
3241
WorkflowContext workflowContext,
3342
Function<WorkflowExecutionCompletableListener, CompletableFuture<?>> function) {

‎impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowMutableInstance.java‎

Lines changed: 102 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,11 @@ public class WorkflowMutableInstance implements WorkflowInstance {
6161
private Lock statusLock = new ReentrantLock();
6262
private Map<CompletableFuture<TaskContext>, TaskContext> suspended;
6363

64-
private Collection<CompletableFuture<?>> cancelables = new ArrayList<>();
64+
private Collection<CompletableFuture<?>> cancelables =
65+
Collections.synchronizedList(new ArrayList<>());
6566

66-
private volatile CompletableFuture<?> cancelPublished = CompletableFuture.completedFuture(null);
67+
private Collection<CompletableFuture<Boolean>> outOfOrderListeners =
68+
Collections.synchronizedList(new ArrayList<>());
6769

6870
protected WorkflowMutableInstance(WorkflowDefinition definition, String id, WorkflowModel input) {
6971
this.id = id;
@@ -107,31 +109,16 @@ protected final CompletableFuture<WorkflowModel> startExecution(
107109
.orElse(input))
108110
.whenComplete(this::setCompleteDate)
109111
.thenApply(this::filterAndValidate)
110-
.thenCompose(this::publishEvents)
112+
.thenCompose(this::publishCompletionEvents)
111113
.exceptionallyCompose(this::handleException))
112-
.handle(this::cleanUpAfterCancelPublished)
114+
.handle(this::cleanUpWaitingListeners)
113115
.thenCompose(Function.identity());
114116
futureRef.set(future);
115117
}
116118
return future;
117119
}
118120

119-
private CompletableFuture<WorkflowModel> cleanUpAfterCancelPublished(
120-
WorkflowModel result, Throwable ex) {
121-
return cancelPublished
122-
.handle(
123-
(__, ___) -> {
124-
cleanUp(result, ex);
125-
return null;
126-
})
127-
.thenCompose(
128-
__ ->
129-
ex == null
130-
? CompletableFuture.completedFuture(result)
131-
: CompletableFuture.failedFuture(ex));
132-
}
133-
134-
private CompletableFuture<WorkflowModel> publishEvents(WorkflowModel model) {
121+
private CompletableFuture<WorkflowModel> publishCompletionEvents(WorkflowModel model) {
135122
return status(WorkflowStatus.COMPLETED)
136123
.thenCompose(
137124
__ ->
@@ -145,19 +132,37 @@ private void setCompleteDate(WorkflowModel result, Throwable ex) {
145132
completedAt = Instant.now();
146133
}
147134

148-
private void cleanUp(WorkflowModel result, Throwable ex) {
135+
private CompletableFuture<WorkflowModel> cleanUpWaitingListeners(
136+
WorkflowModel model, Throwable ex) {
137+
return CompletableFuture.allOf(
138+
outOfOrderListeners.toArray(new CompletableFuture[outOfOrderListeners.size()]))
139+
.handle(this::cleanUp)
140+
.thenCompose(__ -> fromResult(model, ex));
141+
}
142+
143+
private Object cleanUp(Object ignored, Throwable ex) {
149144
additionalObjects.values().stream()
150145
.filter(AutoCloseable.class::isInstance)
151146
.map(AutoCloseable.class::cast)
152147
.forEach(WorkflowUtils::safeClose);
153148
additionalObjects.clear();
154149
workflowContext.definition().removeInstance(this);
150+
outOfOrderListeners.clear();
151+
return ignored;
152+
}
153+
154+
private CompletableFuture<WorkflowModel> fromResult(WorkflowModel model, Throwable ex) {
155+
return ex == null
156+
? CompletableFuture.completedFuture(model)
157+
: CompletableFuture.failedFuture(ex);
155158
}
156159

157160
private CompletableFuture<WorkflowModel> handleException(Throwable exception) {
158161
final Throwable cause =
159162
exception instanceof CompletionException ? exception.getCause() : exception;
160-
if (!(cause instanceof CancellationException)) {
163+
if (cause instanceof CancellationException) {
164+
return CompletableFuture.failedFuture(cause);
165+
} else {
161166
return status(WorkflowStatus.FAULTED)
162167
.thenCompose(
163168
__ ->
@@ -166,7 +171,6 @@ private CompletableFuture<WorkflowModel> handleException(Throwable exception) {
166171
l -> l.onWorkflowFailed(new WorkflowFailedEvent(workflowContext, cause))))
167172
.thenCompose(__ -> CompletableFuture.failedFuture(exception));
168173
}
169-
return CompletableFuture.failedFuture(exception);
170174
}
171175

172176
private WorkflowModel filterAndValidate(WorkflowModel model) {
@@ -232,29 +236,21 @@ public <T> T outputAs(Class<T> clazz) {
232236
: null;
233237
}
234238

235-
public CompletableFuture<Boolean> status(WorkflowStatus state) {
239+
public CompletableFuture<Boolean> status(WorkflowStatus newState) {
236240
WorkflowStatus prevState;
241+
statusLock.lock();
237242
try {
238-
statusLock.lock();
239-
prevState = this.status.get();
240-
if (prevState == WorkflowStatus.CANCELLED) {
241-
// cancellation is final: late callbacks (e.g. an event received by a listen task) must
242-
// not move the instance out of it, and a terminal transition must fail so that neither
243-
// onWorkflowCompleted nor onWorkflowFailed is published for a cancelled instance
244-
return isTerminal(state)
245-
? CompletableFuture.failedFuture(
246-
new CancellationException("Workflow instance " + id + " has been cancelled"))
247-
: CompletableFuture.completedFuture(false);
243+
prevState = status.get();
244+
if (prevState == WorkflowStatus.CANCELLED && newState != WorkflowStatus.CANCELLED) {
245+
return CompletableFuture.failedFuture(
246+
new CancellationException("Workflow has been cancelled"));
247+
} else {
248+
status.set(newState);
248249
}
249-
this.status.set(state);
250250
} finally {
251251
statusLock.unlock();
252252
}
253-
return publishStatusChange(prevState, state);
254-
}
255-
256-
private static boolean isTerminal(WorkflowStatus state) {
257-
return state == WorkflowStatus.COMPLETED || state == WorkflowStatus.FAULTED;
253+
return publishStatusChange(prevState, newState);
258254
}
259255

260256
protected final void setStatus(WorkflowStatus state) {
@@ -263,14 +259,10 @@ protected final void setStatus(WorkflowStatus state) {
263259

264260
private CompletableFuture<Boolean> publishStatusChange(
265261
WorkflowStatus prevState, WorkflowStatus state) {
266-
return prevState != state
267-
? publishEvent(
268-
workflowContext,
269-
l ->
270-
l.onWorkflowStatusChanged(
271-
new WorkflowStatusEvent(workflowContext, prevState, state)))
272-
.thenApply(__ -> true)
273-
: CompletableFuture.completedFuture(false);
262+
return publishEvent(
263+
prevState != state,
264+
workflowContext,
265+
l -> l.onWorkflowStatusChanged(new WorkflowStatusEvent(workflowContext, prevState, state)));
274266
}
275267

276268
@Override
@@ -291,28 +283,30 @@ public boolean suspend() {
291283
WorkflowStatus prevState = internalSuspend();
292284
boolean result = prevState != WorkflowStatus.SUSPENDED;
293285
if (result) {
294-
publishStatusChange(prevState, WorkflowStatus.SUSPENDED)
295-
.thenCompose(
296-
__ ->
297-
publishEvent(
298-
workflowContext,
299-
l -> l.onWorkflowSuspended(new WorkflowSuspendedEvent(workflowContext))));
286+
outOfOrder(
287+
publishStatusChange(prevState, WorkflowStatus.SUSPENDED)
288+
.thenCompose(
289+
changed ->
290+
publishEvent(
291+
changed,
292+
workflowContext,
293+
l ->
294+
l.onWorkflowSuspended(new WorkflowSuspendedEvent(workflowContext)))));
300295
}
301296
return result;
302297
}
303298

304299
@Override
305300
public CompletableFuture<Boolean> suspendFuture() {
306301
WorkflowStatus prevState = internalSuspend();
307-
return prevState != WorkflowStatus.SUSPENDED
308-
? publishStatusChange(prevState, WorkflowStatus.SUSPENDED)
302+
return outOfOrder(
303+
publishStatusChange(prevState, WorkflowStatus.SUSPENDED)
309304
.thenCompose(
310-
__ ->
305+
changed ->
311306
publishEvent(
307+
changed,
312308
workflowContext,
313-
l -> l.onWorkflowSuspended(new WorkflowSuspendedEvent(workflowContext))))
314-
.thenApply(__ -> true)
315-
: CompletableFuture.completedFuture(false);
309+
l -> l.onWorkflowSuspended(new WorkflowSuspendedEvent(workflowContext)))));
316310
}
317311

318312
private WorkflowStatus internalSuspend() {
@@ -338,28 +332,29 @@ public boolean resume() {
338332
WorkflowStatus prevStatus = internalResume();
339333
boolean result = prevStatus != WorkflowStatus.RUNNING;
340334
if (result) {
341-
publishStatusChange(prevStatus, WorkflowStatus.RUNNING)
342-
.thenCompose(
343-
__ ->
344-
publishEvent(
345-
workflowContext,
346-
l -> l.onWorkflowResumed(new WorkflowResumedEvent(workflowContext))));
335+
outOfOrder(
336+
publishStatusChange(prevStatus, WorkflowStatus.RUNNING)
337+
.thenCompose(
338+
changed ->
339+
publishEvent(
340+
changed,
341+
workflowContext,
342+
l -> l.onWorkflowResumed(new WorkflowResumedEvent(workflowContext)))));
347343
}
348344
return result;
349345
}
350346

351347
@Override
352348
public CompletableFuture<Boolean> resumeFuture() {
353349
WorkflowStatus prevStatus = internalResume();
354-
return prevStatus != WorkflowStatus.RUNNING
355-
? publishStatusChange(prevStatus, WorkflowStatus.RUNNING)
350+
return outOfOrder(
351+
publishStatusChange(prevStatus, WorkflowStatus.RUNNING)
356352
.thenCompose(
357-
__ ->
353+
change ->
358354
publishEvent(
355+
change,
359356
workflowContext,
360-
l -> l.onWorkflowResumed(new WorkflowResumedEvent(workflowContext))))
361-
.thenApply(__ -> true)
362-
: CompletableFuture.completedFuture(false);
357+
l -> l.onWorkflowResumed(new WorkflowResumedEvent(workflowContext)))));
363358
}
364359

365360
private WorkflowStatus internalResume() {
@@ -382,13 +377,12 @@ private WorkflowStatus internalResume() {
382377
return result;
383378
}
384379

385-
public CompletableFuture<TaskContext> cancelCheck(TaskContext t) {
380+
public <T> CompletableFuture<T> cancelCheck(T t) {
386381
try {
387382
statusLock.lock();
388383
if (status.get() == WorkflowStatus.CANCELLED) {
389-
CompletableFuture<TaskContext> cancelled = new CompletableFuture<TaskContext>();
390-
cancelled.completeExceptionally(
391-
new CancellationException("Task " + t.taskName() + " has been cancelled"));
384+
CompletableFuture<T> cancelled = new CompletableFuture<>();
385+
cancelled.completeExceptionally(new CancellationException(t + " has been cancelled"));
392386
return cancelled;
393387
}
394388
} finally {
@@ -419,68 +413,53 @@ public CompletableFuture<TaskContext> suspendedCheck(TaskContext t) {
419413

420414
@Override
421415
public boolean cancel() {
422-
CancelRequest request = internalCancel();
423-
if (request.accepted()) {
424-
publishCancelled(request);
425-
}
426-
return request.accepted();
416+
CompletableFuture<Boolean> result = internalCancel();
417+
return !result.isDone() || result.join();
427418
}
428419

429420
@Override
430421
public CompletableFuture<Boolean> cancelFuture() {
431-
CancelRequest request = internalCancel();
432-
return request.accepted()
433-
? publishCancelled(request).thenApply(__ -> true)
434-
: CompletableFuture.completedFuture(false);
435-
}
436-
437-
/**
438-
* Publishes the cancellation and only then cancels the pending futures. Cancelling them may
439-
* complete the execution pipeline synchronously, and the pipeline may also end on its own while
440-
* the cancellation is being published, so {@link #cleanUpAfterCancelPublished} waits for {@link
441-
* CancelRequest#published()} before clearing the instance metadata.
442-
*/
443-
private CompletableFuture<?> publishCancelled(CancelRequest request) {
444-
return publishStatusChange(request.prevStatus(), WorkflowStatus.CANCELLED)
445-
.thenCompose(
446-
__ ->
447-
publishEvent(
448-
workflowContext,
449-
l -> l.onWorkflowCancelled(new WorkflowCancelledEvent(workflowContext))))
450-
.whenComplete(
451-
(__, ex) -> {
452-
request.published().complete(null);
453-
request.toCancel().forEach(t -> t.cancel(true));
454-
});
455-
}
456-
457-
private record CancelRequest(
458-
WorkflowStatus prevStatus,
459-
Collection<CompletableFuture<?>> toCancel,
460-
CompletableFuture<Void> published) {
461-
boolean accepted() {
462-
return prevStatus != WorkflowStatus.CANCELLED;
463-
}
422+
return internalCancel();
464423
}
465424

466-
private CancelRequest internalCancel() {
425+
private CompletableFuture<Boolean> internalCancel() {
426+
WorkflowStatus prevState;
427+
Collection<CompletableFuture<?>> toCancel = null;
467428
try {
468429
statusLock.lock();
469430
if (TaskExecutorHelper.isActive(status.get())) {
470-
Collection<CompletableFuture<?>> toCancel = new ArrayList<>(cancelables);
431+
toCancel = new ArrayList<>(cancelables);
471432
cancelables.clear();
472-
CompletableFuture<Void> published = new CompletableFuture<>();
473-
cancelPublished = published;
474-
return new CancelRequest(status.getAndSet(WorkflowStatus.CANCELLED), toCancel, published);
433+
prevState = status.getAndSet(WorkflowStatus.CANCELLED);
475434
} else {
476-
return new CancelRequest(
477-
WorkflowStatus.CANCELLED,
478-
Collections.emptyList(),
479-
CompletableFuture.completedFuture(null));
435+
prevState = WorkflowStatus.CANCELLED;
480436
}
481437
} finally {
482438
statusLock.unlock();
483439
}
440+
CompletableFuture<Boolean> result =
441+
outOfOrder(
442+
publishStatusChange(prevState, WorkflowStatus.CANCELLED)
443+
.thenCompose(
444+
changed ->
445+
publishEvent(
446+
changed,
447+
workflowContext,
448+
l ->
449+
l.onWorkflowCancelled(
450+
new WorkflowCancelledEvent(workflowContext)))));
451+
452+
if (prevState != WorkflowStatus.CANCELLED && toCancel != null) {
453+
toCancel.forEach(t -> t.cancel(true));
454+
}
455+
return result;
456+
}
457+
458+
private CompletableFuture<Boolean> outOfOrder(CompletableFuture<Boolean> future) {
459+
if (!future.isDone()) {
460+
outOfOrderListeners.add(future);
461+
}
462+
return future;
484463
}
485464

486465
public void addCancelable(CompletableFuture<?> cancelable) {

‎impl/test/src/test/java/io/serverlessworkflow/impl/test/CancelMetadataTest.java‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,23 @@ void metadataIsAvailableWhenPipelineEndsWhileCancellationIsPublished(
160160
.atMost(Duration.ofSeconds(5))
161161
.pollInterval(Duration.ofMillis(5))
162162
.until(() -> instance.status() == WorkflowStatus.WAITING);
163+
164+
// Hold onWorkflowCancelled callback to delay its execution
163165
CompletableFuture<Void> gate = cancelGate.hold(instance.id());
164166

167+
// Cancel: sets CANCELLED status and immediately cancels listen futures
165168
instance.cancel();
169+
170+
// For listen tasks: emit event but subscription is already cancelled (intentional)
171+
// For wait tasks: no event to emit
166172
if (event != null) {
167-
// completes the listen task
168173
emitDefinition.instance(event).start().join();
169174
}
170-
// the pipeline has seen the cancellation while onWorkflowCancelled is still pending
175+
176+
// Pipeline ends due to forced cancellation while onWorkflowCancelled is pending
171177
await().atMost(Duration.ofSeconds(5)).until(() -> listener.taskCancelledSeen(instance.id()));
178+
179+
// Release gate to execute onWorkflowCancelled callback
172180
gate.complete(null);
173181

174182
assertThat(future)

0 commit comments

Comments
 (0)