Repository navigation
Refuse a workflow cancelling, resuming or deleting itself - #661
Merged
Merged
Conversation
Called from a workflow, or from one of its steps, a cancel, resume or delete whose targets include the calling workflow now throws IllegalArgumentException before anything is written. The workflow can catch it and carry on. Each of these clears or removes the caller's own row, so under the ownership fence the caller's next checkpoint fails. A self-cancel ended CANCELLED with no step recorded, a self-delete ended with the workflow gone, and a self-resume put the workflow back on the queue before its own step was recorded: each dispatch replayed to the same resume and did it again, with no end short of deleting the workflow. The check runs inside the call's step, so from a workflow body the refusal is recorded as that step's error and a replay or fork throws it again rather than deciding afresh. From a step body the call takes no step of its own, and the refusal is thrown into the enclosing step. This covers a caller that names itself. Refusing a cancel or delete of a tree the caller belongs to (an ancestor with cancelChildren or deleteChildren) needs the cascade and its step record in one transaction, and comes with that change.
Assert that a refusal inside a step records no cancel step of its own, name the step test for all three operations it covers, inline the one-use launch helper, and release blocked workflows in a finally so a failed assertion doesn't leave them parked until shutdown.
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Workflow-body refusal errors can lose their exception type during replay with a type-losing serializer, causing workflows that initially handle the error to fail on recovery or fork.
1 open issue
What changed in this PR
This PR prevents workflows from explicitly cancelling, resuming, or deleting themselves, including calls made inside their steps. Management calls targeting other workflows remain unchanged; ancestor-tree protection is deferred to #656.
Changes:
- Adds self-target checks inside internal management steps, before target mutations.
- Documents the new
IllegalArgumentExceptionbehavior across public API overloads. - Adds refusal and replay tests, replacing tests for the previous self-target behavior.
| File | Description |
|---|---|
transact/src/test/java/dev/dbos/transact/workflow/SelfTargetTest.java |
Tests refusals, replay, bulk-call safety, and allowed operations. |
transact/src/test/java/dev/dbos/transact/workflow/OwnershipFenceTest.java |
Removes tests and helpers for the previous self-target behavior. |
transact/src/main/java/dev/dbos/transact/execution/DBOSExecutor.java |
Adds shared self-target validation for cancel, resume, and delete. |
transact/src/main/java/dev/dbos/transact/DBOS.java |
Documents self-target exceptions on public management methods. |
Review effort: Balanced
kraftp
approved these changes
Nov 12, 2026
A built-in step recorded what it threw with the configured serializer, so a custom serializer that doesn't keep exception types rebuilt a different type on replay. A workflow that caught a refused self-cancel's IllegalArgumentException on its first run would then fail when recovered or forked, instead of taking the same branch. DBOS's own steps now record their errors natively whatever serializer is configured, as a child start's refusal already is. DBOS.getResult keeps the configured serializer: what it records is the awaited workflow's own error, which belongs to the application. User steps are unchanged, and replay reads the recorded format, so it needs no change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Called from inside a workflow, or from one of its steps,
cancelWorkflow(s),resumeWorkflow(s)anddeleteWorkflow(s)now throwIllegalArgumentExceptionwhen their targets include the calling workflow. Nothing is written, and the workflow can catch the exception and carry on.Why
Each of these clears or removes the caller's own
workflow_statusrow, so under the ownership fence the caller's next checkpoint fails:CANCELLED, with its cancel step never recorded;How
The check runs first inside the call's internal step, before any database call:
DBOS.cancelWorkflow/DBOS.resumeWorkflow/DBOS.deleteWorkflow, so a recovery or fork rethrows it instead of deciding again.Calls with no workflow context (outside a workflow,
DBOSClient, Conductor, the admin server) are unaffected, as are calls that target other workflows, including the caller's own children's trees.This PR refuses only a caller that names itself. Refusing a cancel or delete of an ancestor's tree that contains the caller (
cancelChildren/deleteChildren) needs the cascade and its step record in one transaction, and comes with #656.Tests
SelfTargetTestcovers:OwnershipFenceTest: removes the three tests that recorded the old self-cancel, self-delete and self-resume behaviour.🤖 Generated with Claude Code