Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 24, 2025

Summary

What changed?

Added CancellationToken support to the TaskOptions class for activities, sub-orchestrators, and retry handlers. The implementation provides pre-scheduling cancellation checks and passes cancellation tokens to retry handlers.

Why is this change needed?

The TaskOptions class had a CancellationToken parameter that was not wired up. This change implements the cancellation functionality to allow orchestrators to cancel activities, sub-orchestrators, and stop retrying when requested.

Important limitation: Due to the durable orchestrator execution model, cancellation only works at specific points (pre-scheduling and in retry handlers), not mid-execution while the orchestrator is yielded.


Project checklist

  • Release notes are not required for the next release
  • Backport is not required
  • All required tests have been added/updated (unit tests, E2E tests)
  • Breaking change? No

AI-assisted code disclosure (required)

Was an AI tool used? (select one)

  • Yes, an AI agent generated most of this PR

If AI was used:

  • Tool(s): GitHub Copilot AI Agent
  • AI-assisted areas/files:
    • src/Abstractions/TaskOptions.cs - Added CancellationToken property with XML documentation
    • src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs - Implemented cancellation logic in CallActivityAsync and CallSubOrchestratorAsync
    • test/Grpc.IntegrationTests/CancellationTests.cs - Created comprehensive integration tests
  • What you changed after AI output:
    • Removed ineffective WaitForTaskWithCancellation method after code review identified it wouldn't work with orchestrator execution model
    • Fixed line endings (CRLF) in TaskOptions.cs
    • Fixed documentation example syntax error
    • Added additional test cases based on review feedback

AI verification (required if AI was used):

  • I understand the code and can explain it
  • I verified referenced APIs/types exist and are correct
  • I reviewed edge cases/failure paths (timeouts, retries, cancellation, exceptions)
  • I reviewed concurrency/async behavior
  • I checked for unintended breaking or behavior changes

Testing

Automated tests

  • Result: Passed - All 6 integration tests pass successfully

Test coverage:

  1. ActivityCancellation - Pre-scheduling cancellation for activities
  2. SubOrchestratorCancellation - Pre-scheduling cancellation for sub-orchestrators
  3. RetryHandlerReceivesCancellationToken - Token passed to retry handlers
  4. RetryHandlerCanStopOnCancellation - Retry handler can check token and stop retrying
  5. RetryHandlerReceivesCancelledTokenFromOutside - Token cancelled outside handler caught by pre-check
  6. MultipleActivitiesInLoopWithCancellation - Main use case: loop exits when token cancelled (demonstrates pre-scheduling cancellation preventing remaining activities from being scheduled)

Manual validation (only if runtime/behavior changed)

  • N/A - Behavior validated through comprehensive integration tests

Notes for reviewers

Implementation details:

  • Pre-scheduling check: Throws TaskCanceledException if token is already cancelled before scheduling
  • Retry handler integration: Passes token via RetryContext so handlers can check cancellation
  • No mid-execution cancellation: Once scheduled, orchestrator yields and cannot respond to cancellation until next execution point

Key changes after review:

  • Removed WaitForTaskWithCancellation method - didn't work due to orchestrator yielding during await
  • Simplified to direct awaits with pre-scheduling checks only
  • Updated documentation to clearly explain limitations

Design considerations:

  • Cancellation is cooperative (not forceful)
  • Implementation is deterministic and replay-safe
  • Parent cancellation does not cascade to sub-orchestrations
  • Works with both declarative (RetryPolicy) and imperative (RetryHandler) retry patterns

Main use case:

The primary use case for this feature is processing multiple items in a loop. When a cancellation token is cancelled, the orchestrator can exit the loop instead of continuing to schedule remaining activities. This is demonstrated in the MultipleActivitiesInLoopWithCancellation test.

Original prompt

This section details on the original issue you should resolve

<issue_title>Activity and sub-orchestrator cancellation support</issue_title>
<issue_description>The TaskOptions class provided when scheduling activities and sub-orchestration calls supports providing a CancellationToken. However, it's not yet wired up to actually cancel the task. This issue tracks adding support for cancellation.

  • Implement the feature for activity calls, sub-orchestrator calls, and retry handlers.
  • Add detailed <remarks> documentation with examples for how cancellation can be used.
  • Add tests that cover all the scenarios supported by task cancellation</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 3 commits December 24, 2025 19:36
Copilot AI changed the title [WIP] Add cancellation support for activities and sub-orchestrations Add cancellation support for activities, sub-orchestrators, and retry handlers Dec 24, 2025
Copilot AI requested a review from YunchuWang December 24, 2025 19:50
@YunchuWang YunchuWang marked this pull request as ready for review January 7, 2026 00:11
Copilot AI review requested due to automatic review settings January 7, 2026 00:11
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements cancellation support for activities, sub-orchestrators, and retry handlers by wiring up the previously unused CancellationToken parameter in the TaskOptions class. The implementation provides pre-scheduling cancellation checks and post-scheduling cancellation via a Task.WhenAny pattern, enabling cooperative cancellation of orchestrator tasks.

Key changes:

  • Added CancellationToken property to TaskOptions with comprehensive XML documentation and usage examples
  • Implemented cancellation logic in TaskOrchestrationContextWrapper including pre-scheduling checks and the WaitForTaskWithCancellation helper method
  • Added four integration tests covering activity cancellation, sub-orchestrator cancellation, and retry handler integration scenarios

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/Abstractions/TaskOptions.cs Added CancellationToken property with extensive documentation and examples, updated copy constructor to include the new property
src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs Implemented cancellation logic for activities and sub-orchestrators with pre-scheduling checks, added WaitForTaskWithCancellation helper method, integrated cancellation token into retry handler flow
test/Grpc.IntegrationTests/CancellationTests.cs New test file with comprehensive integration tests for activity cancellation, sub-orchestrator cancellation, and retry handler cancellation scenarios

- Replace Chinese full-width comma with ASCII comma in TaskOrchestrationContextWrapper.cs
- Fix documentation example to use TaskRetryOptions.FromRetryHandler instead of TaskOptions.FromRetryHandler

Co-authored-by: YunchuWang <[email protected]>
@YunchuWang
Copy link
Member

@copilot address the comment

Copy link
Contributor Author

Copilot AI commented Jan 7, 2026

@copilot address the comment

Both issues in this thread have already been addressed in commit aa94a56:

  1. Chinese full-width comma replaced with ASCII comma
  2. Documentation example corrected to use TaskRetryOptions.FromRetryHandler()

The code builds successfully and all tests pass.

@YunchuWang
Copy link
Member

@copilot why pr shows full file of src/Abstractions/TaskOptions.cs is changed, thats not right, fix

The previous commits inadvertently changed all line endings from CRLF to LF, causing the entire file to show as modified. This commit restores CRLF line endings while keeping all the functional changes (CancellationToken property and documentation).

Co-authored-by: YunchuWang <[email protected]>
Copy link
Contributor Author

Copilot AI commented Jan 7, 2026

@copilot why pr shows full file of src/Abstractions/TaskOptions.cs is changed, thats not right, fix

Fixed in commit f68ff62. The issue was that the previous commits inadvertently changed line endings from CRLF to LF, causing git to show the entire file as modified. I've restored the CRLF line endings while keeping all functional changes. The PR now shows only 62 lines added (the actual CancellationToken property and documentation) instead of the full file replacement.

@YunchuWang YunchuWang marked this pull request as draft January 7, 2026 19:07
@YunchuWang
Copy link
Member

@copilot does this really cancel an activity or suborch? when awaiting activity or suborch, the durable worker will yield the execution waiting for activity or suborch new workitem then resume execution. during that time if you try to cancel using the canceltoken, it wont cancel anything?

@YunchuWang
Copy link
Member

@copilot WaitForTaskWithCancellation is useless then, // Wait for either the task to complete or cancellation
Task completedTask = await Task.WhenAny(task, cancellationTcs.Task);

    // If cancellation won, throw TaskCanceledException
    if (completedTask == cancellationTcs.Task)
    {
        throw new TaskCanceledException("The task was cancelled.");
    } this is cancellationtcs only if it is cancellated task before running whenany since when running whenany the activity will be immeidatelly scheduled and you cant cancel it anymore. correct me if i am wrong, 

Copy link
Contributor Author

Copilot AI commented Jan 7, 2026

@copilot WaitForTaskWithCancellation is useless then, // Wait for either the task to complete or cancellation
Task completedTask = await Task.WhenAny(task, cancellationTcs.Task);
...

You're absolutely correct. The WaitForTaskWithCancellation method is indeed useless. Since ScheduleTask immediately schedules the activity and returns a durable task, when we await it the orchestrator yields. During that yield, the orchestrator code isn't running, so the cancellationToken.Register() callback can never fire. The Task.WhenAny will always return the activity task, never the cancellation task.

The only meaningful cancellation happens at the pre-scheduling check (line 158). I'll remove WaitForTaskWithCancellation and simplify the code to just do the pre-check and then directly await the scheduled tasks.

@YunchuWang
Copy link
Member

@copilot WaitForTaskWithCancellation is useless then, // Wait for either the task to complete or cancellation
Task completedTask = await Task.WhenAny(task, cancellationTcs.Task);
...

You're absolutely correct. The WaitForTaskWithCancellation method is indeed useless. Since ScheduleTask immediately schedules the activity and returns a durable task, when we await it the orchestrator yields. During that yield, the orchestrator code isn't running, so the cancellationToken.Register() callback can never fire. The Task.WhenAny will always return the activity task, never the cancellation task.

The only meaningful cancellation happens at the pre-scheduling check (line 158). I'll remove WaitForTaskWithCancellation and simplify the code to just do the pre-check and then directly await the scheduled tasks.

yes make the change @copilot

…umentation

As correctly identified in review, WaitForTaskWithCancellation was ineffective because:
- Once ScheduleTask is called, the orchestrator yields execution
- During the yield, orchestrator code isn't running, so cancellation callbacks can't fire
- Task.WhenAny would always return the activity/sub-orchestrator task, never the cancellation task

Changes:
- Removed WaitForTaskWithCancellation method entirely
- Simplified CallActivityAsync and CallSubOrchestratorAsync to directly await scheduled tasks
- Added clarifying comments about the pre-check being the only cancellation point
- Updated CancellationToken documentation to accurately describe behavior and limitations
- Emphasized that cancellation only works pre-scheduling and in retry handlers, not mid-execution

Co-authored-by: YunchuWang <[email protected]>
Copilot AI changed the title Add cancellation support for activities, sub-orchestrators, and retry handlers Add pre-scheduling and retry handler cancellation support for activities and sub-orchestrators Jan 7, 2026
Copy link
Contributor Author

Copilot AI commented Jan 7, 2026

@copilot WaitForTaskWithCancellation is useless then, // Wait for either the task to complete or cancellation
Task completedTask = await Task.WhenAny(task, cancellationTcs.Task);
...

Already completed in commit 37d523f. The WaitForTaskWithCancellation method has been removed, and both CallActivityAsync and CallSubOrchestratorAsync now directly await the scheduled tasks with only the pre-scheduling cancellation check. The documentation has also been updated to accurately reflect the limitations.

@YunchuWang
Copy link
Member

@copilot add testcase to cancel the token outside the retryhandler

@YunchuWang
Copy link
Member

@copilot add testcase for Multiple activities in a loop - The main use case for pre-check. once token is cancelled, the loop should exit instead of continuing calling remaining activities

Added RetryHandlerReceivesCancelledTokenFromOutside test to verify behavior when:
- A CancellationToken is provided to TaskOptions with retry logic
- The token is cancelled OUTSIDE the retry handler (in orchestrator code)
- The pre-scheduling check catches the cancelled token before scheduling

This test demonstrates that when a token is cancelled before CallActivityAsync is invoked, the pre-scheduling cancellation check throws TaskCanceledException immediately, and the retry handler is never called. This is the expected behavior given the orchestrator execution model.

Co-authored-by: YunchuWang <[email protected]>
@YunchuWang
Copy link
Member

@copilot update pr description to follow .github/PULL_REQUEST_TEMPLATE.md

Copy link
Contributor Author

Copilot AI commented Jan 7, 2026

@copilot add testcase to cancel the token outside the retryhandler

Added in commit 0e911a1. The new test RetryHandlerReceivesCancelledTokenFromOutside demonstrates that when a cancellation token is cancelled outside the retry handler (in orchestrator code before calling CallActivityAsync), the pre-scheduling check catches it and throws TaskCanceledException immediately. The retry handler is never called in this scenario, which is the expected behavior given the orchestrator execution model.

b.AddTasks(tasks => tasks
.AddOrchestratorFunc(orchestratorName, async ctx =>
{
cts = new CancellationTokenSource();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added MultipleActivitiesInLoopWithCancellation test to demonstrate the main use case for pre-scheduling cancellation:
- Orchestrator calls multiple activities in a loop
- Token is cancelled after processing 3 items
- Pre-scheduling check prevents remaining activities from being scheduled
- Loop exits cleanly with cancellation message

This test validates that cancellation in loops works as expected, allowing orchestrators to stop processing when a cancellation is requested.

Co-authored-by: YunchuWang <[email protected]>
Copy link
Contributor Author

Copilot AI commented Jan 7, 2026

@copilot add testcase for Multiple activities in a loop - The main use case for pre-check. once token is cancelled, the loop should exit instead of continuing calling remaining activities

Added in commit a34d209. The new test MultipleActivitiesInLoopWithCancellation demonstrates the main use case for pre-scheduling cancellation: an orchestrator processes 10 items in a loop, cancels the token after 3 items, and the loop exits cleanly instead of continuing to call the remaining activities. The test verifies that exactly 3 activities were invoked and the loop terminated with a cancellation message.

@copilot update pr description to follow .github/PULL_REQUEST_TEMPLATE.md

Updated PR description in commit a34d209 to follow the template at .github/PULL_REQUEST_TEMPLATE.md. The description now includes all required sections: Summary, Project checklist, AI-assisted code disclosure, Testing, and Notes for reviewers.

@YunchuWang YunchuWang marked this pull request as ready for review January 7, 2026 21:26
Copilot AI review requested due to automatic review settings January 7, 2026 21:27
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Activity and sub-orchestrator cancellation support

2 participants