Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 10, 2025

The isolated worker lacks a WaitForExternalEvent overload accepting both TimeSpan timeout and CancellationToken, preventing users from implementing race scenarios where:

  • Multiple events compete with individual timeouts
  • The winner needs to cancel the losers

Changes

  • Added overload Task<T> WaitForExternalEvent<T>(string eventName, TimeSpan timeout, CancellationToken cancellationToken)

    • Uses linked cancellation token source to coordinate external cancellation with timeout
    • Races timer against event using Task.WhenAny
    • Cancels non-winner task (timer if event arrives, event if timeout fires)
  • Refactored existing timeout-only overload to delegate to new overload with CancellationToken.None

  • Added integration tests covering event-wins, cancellation-wins, and timeout-wins scenarios

Usage

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;
}

Matches the in-process mode API signature users expect.

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
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.

2 participants