Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 10, 2025

Summary

What changed?

  • Added new overload Task<T> WaitForExternalEvent<T>(string eventName, TimeSpan timeout, CancellationToken cancellationToken) to TaskOrchestrationContext
  • Refactored existing timeout-only overload to delegate to the new overload with CancellationToken.None
  • Added 4 integration tests covering event-wins, cancellation-wins, timeout-wins, and external-cancellation-wins scenarios

Why is this change needed?

Users migrating from in-process mode to isolated mode lost the ability to use WaitForExternalEvent with both a timeout and a cancellation token. This prevented implementing race scenarios where multiple events compete with individual timeouts and the winner needs to cancel the losers.

Issues / work items

  • Related to WaitForExternalEvent API parity between in-process and isolated modes

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/TaskOrchestrationContext.cs - New overload implementation and refactoring
    • test/Grpc.IntegrationTests/OrchestrationPatterns.cs - All 4 integration tests
  • What you changed after AI output: None

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 4 new integration tests pass (EventWins, CancellationWins, TimeoutWins, ExternalCancellationWins)
  • All existing integration tests (109+) continue to pass

Manual validation (only if runtime/behavior changed)

  • N/A (pure API addition, fully validated through automated tests)

Notes for reviewers

  • This is a purely additive API change with no breaking changes
  • Matches the in-process mode API signature that users expect
  • Uses linked cancellation token source to coordinate external cancellation with internal timeout
  • Existing timeout-only overload delegates to new overload to eliminate code duplication

Usage Example

using var cts1 = new CancellationTokenSource();
using var cts2 = new CancellationTokenSource();

Task<string> event1 = context.WaitForExternalEvent<string>("Event1", TimeSpan.FromDays(7), cts1.Token);
Task<string> event2 = context.WaitForExternalEvent<string>("Event2", TimeSpan.FromDays(7), cts2.Token);

Task winner = await Task.WhenAny(event1, event2);
if (winner == event1)
{
    cts2.Cancel();
    return await event1;
}
else
{
    cts1.Cancel();
    return await event2;
}
Original prompt

This section details on the original issue you should resolve

<issue_title>WaitForExternalEvent(eventName, timespan, cancellationToken) not supported (anymore?) in isolated mode</issue_title>
<issue_description>Hi there,

Context / issue

We came across an API change in the WaitForExternalEvent<T> in the Microsoft.DurableTask package as we had the code below in place for in-process, wait for external events for 7 days, or when the cancellationtoken has ben cancelled when the winner prevails:

Task<string> event1Waiter = context.WaitForExternalEvent<string>(Constants.Event1, TimeSpan.FromDays(7), ctsEvent1.Token);
Task<string> event2Waiter = context.WaitForExternalEvent<string>(Constants.Event2, TimeSpan.FromDays(7), ctsEvent2.Token);

Now, in isolated mode, we don't have the option to cancel the token externally, and are forced to use a timer to achieve the 'same' functionality as we had in the in-process mode. The only options we have are:
image

Expected result

To be able to pass in the cancellationtoken next to the timespan as we could in the in-process variant, to cancel the other tasks when one task prevails as winner.

Below some code snippets to demonstrate the issue I try to describe here. Feel free to inform me to clarify or elaborate on certain parts.

Regards,
Tom

Not working:

 [Function(Constants.OrchestratorName)]
 public async Task<string> NotWorkingWithTimespan([OrchestrationTrigger] TaskOrchestrationContext context)
 {
     var input = context.GetInput<string>();

     _logger.LogDebug("Started orchestrator with input: '{input}'", input);
     
     context.SetCustomStatus("Awaiting events");
    
     Task<string> event1Waiter = context.WaitForExternalEvent<string>(Constants.Event1, TimeSpan.FromDays(7));
     Task<string> event2Waiter = context.WaitForExternalEvent<string>(Constants.Event2, TimeSpan.FromDays(7));

     var winner = await Task.WhenAny(event1Waiter, event2Waiter);

     if (winner == event1Waiter)
     {
         //Because event1Waiter is the winner, we need to cancel the event2Waiter task via a CTS
         // This is not possible anymore from the current implementation... 
         // In the in-proc mode we cancel the token for event2 here to cancel the event2Waiter.. 
         _logger.LogDebug("Wait for event1 winner: '{data}'", event1Waiter.Result);
         return $"OrchestratorCompleted with result: '{event1Waiter.Result}'";
     }

     if (winner == event2Waiter)
     {
         //Because event2Waiter is the winner, we need to cancel the event1Waiter task via a CTS
         // This is not possible anymore from the current implementations... 
         // In the in-proc mode we cancel the token for event1 here to cancel the event1Waiter.. 
         _logger.LogDebug("Wait for event2 winner: '{data}'", event2Waiter.Result);
         return $"Orchestrator completed with result: '{event2Waiter.Result}'";
     }

     return "Received timeout...";
 }

Not working with timespan and timertask either

[Function(Constants.OrchestratorName)]
public async Task<string> NotWorkingWithTimeSpanAndTimerEither([OrchestrationTrigger] TaskOrchestrationContext context)
{
    var input = context.GetInput<string>();

    _logger.LogDebug("Started orchestrator with input: '{input}'", input);
    
    context.SetCustomStatus("Awaiting events");

    using var timerCts = new CancellationTokenSource();

    Task<string> event1Waiter = context.WaitForExternalEvent<string>(Constants.Event1, TimeSpan.FromDays(7));
    Task<string> event2Waiter = context.WaitForExternalEvent<string>(Constants.Event2, TimeSpan.FromDays(7));
    Task timerTask = context.CreateTimer(TimeSpan.FromSeconds(10), timerCts.Token);
    var winner = await Task.WhenAny(event1Waiter, event2Waiter, timerTask);

    if (winner == event1Waiter)
    {      
        timerCts.Cancel();
        _logger.LogDebug("Wait for event1 winner: '{data}'", event1Waiter.Result);
        return $"OrchestratorCompleted with result: '{event1Waiter.Result}'";
    }

    if (winner == event2Waiter)
    {     
        timerCts.Cancel();
        _logger.LogDebug("Wait for event2 winner: '{data}'", event2Waiter.Result);
        return $"Orchestrator completed with result: '{event2Waiter.Result}'";
    }

    if (winner == timerTask)
    {
        return "Received timeout...";
    }
    return "Received timeout...";
}

Working with timertask:

 [Function(Constants.OrchestratorName)]
 public async Task<string> WorkingWithTimer([OrchestrationTrigger] TaskOrchestrationContext context)
 {
     var input = context.GetInput<string>();

     _logger.LogDebug("Started orchestrator with input: '{input}'", input);
     context.SetCustomStatus("Awaiting events");

...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes microsoft/durabletask-dotnet#277

<!-- START COPILOT CODING AGENT TIPS -->
---Let Copilot coding agent [set things up for you](https://github.com/microsoft/durabletask-dotnet/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Fix support for WaitForExternalEvent in isolated mode Add WaitForExternalEvent overload with timeout and cancellation token Dec 10, 2025
Copilot AI requested a review from YunchuWang December 10, 2025 23:06
@torosent torosent force-pushed the copilot/fix-wait-for-external-event branch from 8d456b7 to da251bc Compare December 15, 2025 21:43
@torosent
Copy link
Member

change the PR description to the latest PR template in https://github.com/microsoft/durabletask-dotnet/blob/main/.github/PULL_REQUEST_TEMPLATE.md

@YunchuWang YunchuWang marked this pull request as ready for review December 23, 2025 20:54
Copilot AI review requested due to automatic review settings December 23, 2025 20:54
@YunchuWang
Copy link
Member

@copilot add test for External cancellation token fires

@YunchuWang
Copy link
Member

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

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 adds a missing WaitForExternalEvent<T> overload that accepts both a timeout and cancellation token, enabling users to implement race scenarios where multiple events compete with individual timeouts and the winner can cancel the losers.

Key changes:

  • Added new overload WaitForExternalEvent<T>(string eventName, TimeSpan timeout, CancellationToken cancellationToken) that combines timeout and external cancellation using linked cancellation token sources
  • Refactored the timeout-only overload to delegate to the new overload with CancellationToken.None
  • Added comprehensive integration tests covering event-wins, cancellation-wins, and timeout-wins scenarios

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/Abstractions/TaskOrchestrationContext.cs Added new overload combining timeout and cancellation token functionality; refactored existing timeout overload to delegate to new implementation
test/Grpc.IntegrationTests/OrchestrationPatterns.cs Added three integration tests validating event arrival, external cancellation, and timeout scenarios for the new API

After thoroughly reviewing this pull request, I found no issues with the implementation. The code is well-written, properly documented following existing patterns, and includes comprehensive test coverage. The implementation correctly uses a linked cancellation token source to coordinate external cancellation with timeout, and the race condition between timer and event is properly handled.

Copy link
Contributor Author

Copilot AI commented Dec 23, 2025

@copilot add test for External cancellation token fires

Added test WaitForExternalEvent_WithTimeoutAndCancellationToken_ExternalCancellationWins that validates the behavior when the external cancellation token is explicitly cancelled before the event arrives or timeout fires. Commit: 6554f2b

change the PR description to the latest PR template in https://github.com/microsoft/durabletask-dotnet/blob/main/.github/PULL_REQUEST_TEMPLATE.md

Summary

What changed?

  • Added new overload Task<T> WaitForExternalEvent<T>(string eventName, TimeSpan timeout, CancellationToken cancellationToken) to TaskOrchestrationContext
  • Refactored existing timeout-only overload to delegate to the new overload
  • Added 4 integration tests covering event-wins, cancellation-wins, timeout-wins, and external-cancellation-wins scenarios

Why is this change needed?

Users migrating from in-process mode to isolated mode lost the ability to use WaitForExternalEvent with both a timeout and a cancellation token, preventing them from implementing race scenarios where multiple events compete with individual timeouts and the winner needs to cancel the losers.

Issues / work items

  • Resolves #[issue number from problem statement]

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/TaskOrchestrationContext.cs - New overload and refactoring
    • test/Grpc.IntegrationTests/OrchestrationPatterns.cs - All 4 integration tests
  • What you changed after AI output: N/A

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 4 new integration tests pass
  • All existing integration tests (109) continue to pass

Manual validation (only if runtime/behavior changed)

  • N/A (pure API addition, validated through automated tests)

Notes for reviewers

  • This is a purely additive API change that adds a missing overload to match in-process mode functionality
  • Uses linked cancellation token source to coordinate external cancellation with timeout
  • Existing timeout-only overload now delegates to new overload with CancellationToken.None to eliminate code duplication

Copy link
Contributor Author

Copilot AI commented Dec 23, 2025

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

Here is the PR description formatted according to .github/PULL_REQUEST_TEMPLATE.md:


Summary

What changed?

  • Added new overload Task<T> WaitForExternalEvent<T>(string eventName, TimeSpan timeout, CancellationToken cancellationToken) to TaskOrchestrationContext
  • Refactored existing timeout-only overload to delegate to the new overload with CancellationToken.None
  • Added 4 integration tests covering event-wins, cancellation-wins, timeout-wins, and external-cancellation-wins scenarios

Why is this change needed?

Users migrating from in-process mode to isolated mode lost the ability to use WaitForExternalEvent with both a timeout and a cancellation token. This prevented implementing race scenarios where multiple events compete with individual timeouts and the winner needs to cancel the losers.

Issues / work items

  • Resolves #[issue number - please update]

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/TaskOrchestrationContext.cs - New overload implementation and refactoring
    • test/Grpc.IntegrationTests/OrchestrationPatterns.cs - All 4 integration tests
  • What you changed after AI output: None

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 4 new integration tests pass (EventWins, CancellationWins, TimeoutWins, ExternalCancellationWins)
  • All existing integration tests (109+) continue to pass

Manual validation (only if runtime/behavior changed)

  • N/A (pure API addition, fully validated through automated tests)

Notes for reviewers

  • This is a purely additive API change with no breaking changes
  • Matches the in-process mode API signature that users expect
  • Uses linked cancellation token source to coordinate external cancellation with internal timeout
  • Existing timeout-only overload delegates to new overload to eliminate code duplication

@YunchuWang
Copy link
Member

related #277

@YunchuWang YunchuWang merged commit e0ee326 into main Dec 24, 2025
8 checks passed
@YunchuWang YunchuWang deleted the copilot/fix-wait-for-external-event branch December 24, 2025 00:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants