Skip to content

Add CreateCallbackAsync and WaitForCallbackAsync (DOTNET-8660)#2373

Draft
GarrettBeatty wants to merge 1 commit into
gcbeatty/durable-wave0from
gcbeatty/durable-callbacks
Draft

Add CreateCallbackAsync and WaitForCallbackAsync (DOTNET-8660)#2373
GarrettBeatty wants to merge 1 commit into
gcbeatty/durable-wave0from
gcbeatty/durable-callbacks

Conversation

@GarrettBeatty
Copy link
Copy Markdown
Contributor

@GarrettBeatty GarrettBeatty commented May 14, 2026

#2216

Summary

Adds callback support to the .NET Durable Execution SDK:

  • CreateCallbackAsync<T> returns an ICallback<T> handle that suspends the workflow until an external system delivers a result via the durable execution service.
  • WaitForCallbackAsync<T> composes CreateCallback + a submitter step + GetResultAsync inside a child context for the common "submit and wait" pattern.

Stacked on top of #2372 (Wave 0 cross-cutting types).

Fixes DOTNET-8660.

Public surface

  • IDurableContext.CreateCallbackAsync<T> (single overload)
  • IDurableContext.WaitForCallbackAsync<T> (single overload)
  • ICallback<T> with CallbackId and GetResultAsync
  • IWaitForCallbackContext (Logger only) for submitter functions
  • CallbackConfig (Timeout + HeartbeatTimeout, validates sub-second values)
  • WaitForCallbackConfig : CallbackConfig adds RetryStrategy
  • Exception subclass tree: CallbackException base + CallbackFailedException, CallbackTimeoutException, CallbackSubmitterException

Both APIs read the ILambdaSerializer from ILambdaContext.Serializer (typically registered via LambdaBootstrapBuilder.Create(handler, serializer)) and throw InvalidOperationException if no serializer is registered. AOT and reflection-based scenarios share a single overload — the AOT story is determined entirely by the registered serializer (e.g., SourceGeneratorLambdaJsonSerializer<TContext> for AOT).

Internal

  • CallbackOperation<T> handles fresh execution sync-flush of START with service-allocated CallbackId, deferred error propagation, and replay for SUCCEEDED / FAILED / TIMED_OUT / STARTED / PENDING. Unknown statuses throw NonDeterministicExecutionException.
  • LambdaDurableServiceClient gains an onNewOperations callback so the freshly-allocated CallbackId from NewExecutionState flows back into ExecutionState during the START flush.
  • WaitForCallback's error mapping preserves subclass fidelity on parent-CONTEXT-FAILED replay (CallbackTimeoutException remains CallbackTimeoutException, etc.).

Test plan

  • Build clean (zero warnings, TreatWarningsAsErrors enforced) on net8.0 and net10.0
  • All unit tests pass (203 total: 161 base wave-0 tests + 42 new callback/wait-for-callback tests)
  • 5 new integration tests build successfully (require AWS credentials to run): CreateCallbackHappyPath, CallbackTimeout, CallbackFailed, WaitForCallbackHappyPath, WaitForCallbackSubmitterFails

🤖 Generated with Claude Code


COPY bin/publish/ ${LAMBDA_TASK_ROOT}

ENTRYPOINT ["/var/task/bootstrap"]

COPY bin/publish/ ${LAMBDA_TASK_ROOT}

ENTRYPOINT ["/var/task/bootstrap"]

COPY bin/publish/ ${LAMBDA_TASK_ROOT}

ENTRYPOINT ["/var/task/bootstrap"]

COPY bin/publish/ ${LAMBDA_TASK_ROOT}

ENTRYPOINT ["/var/task/bootstrap"]

COPY bin/publish/ ${LAMBDA_TASK_ROOT}

ENTRYPOINT ["/var/task/bootstrap"]
@GarrettBeatty GarrettBeatty force-pushed the gcbeatty/durable-wave0 branch from 464c591 to d308c3b Compare May 14, 2026 21:49
@GarrettBeatty GarrettBeatty force-pushed the gcbeatty/durable-callbacks branch 2 times, most recently from 951fcd1 to 1c88461 Compare May 14, 2026 22:19
@GarrettBeatty GarrettBeatty force-pushed the gcbeatty/durable-wave0 branch from d308c3b to be4c3ad Compare May 18, 2026 15:23
Adds callback support to the .NET Durable Execution SDK. CreateCallbackAsync
returns an ICallback<T> handle (CallbackId + GetResultAsync) that suspends
the workflow until an external system delivers a result via the durable
execution service. WaitForCallbackAsync composes CreateCallback + a
submitter step + GetResultAsync inside a child context for the common
"submit and wait" pattern.

Public surface:
- IDurableContext.CreateCallbackAsync<T> (single overload)
- IDurableContext.WaitForCallbackAsync<T> (single overload)
- ICallback<T> with CallbackId and GetResultAsync
- IWaitForCallbackContext (Logger only) for submitter functions
- CallbackConfig (Timeout + HeartbeatTimeout, validates sub-second values)
- WaitForCallbackConfig : CallbackConfig adds RetryStrategy
- Exception subclass tree: CallbackException base + CallbackFailedException,
  CallbackTimeoutException, CallbackSubmitterException

Both APIs read the ILambdaSerializer from ILambdaContext.Serializer
(typically registered via LambdaBootstrapBuilder.Create(handler, serializer))
and throw InvalidOperationException if no serializer is registered. AOT and
reflection-based scenarios share a single overload — the AOT story is
determined by the registered serializer.

Internal:
- CallbackOperation<T> handles fresh execution sync-flush of START with
  service-allocated CallbackId, deferred error propagation, and replay
  for SUCCEEDED/FAILED/TIMED_OUT/STARTED/PENDING. Unknown statuses throw
  NonDeterministicExecutionException.
- LambdaDurableServiceClient gains an onNewOperations callback so the
  freshly-allocated CallbackId from NewExecutionState flows back into
  ExecutionState during the START flush.
- WaitForCallback's error mapping preserves subclass fidelity on
  parent-CONTEXT-FAILED replay (CallbackTimeoutException remains
  CallbackTimeoutException, etc.).

Adds unit tests + integration tests covering happy path, timeout,
failure, submitter failure, replay determinism, and replay of each
exception subtype.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@GarrettBeatty GarrettBeatty force-pushed the gcbeatty/durable-callbacks branch from 1c88461 to 5cc9a04 Compare May 18, 2026 15:46
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