Skip to content

Add WaitForConditionAsync polling primitive (DOTNET-8665)#2376

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

Add WaitForConditionAsync polling primitive (DOTNET-8665)#2376
GarrettBeatty wants to merge 1 commit into
gcbeatty/durable-wave0from
gcbeatty/durable-waitforcondition

Conversation

@GarrettBeatty
Copy link
Copy Markdown
Contributor

Summary

Adds service-mediated polling to the .NET Durable Execution SDK. WaitForConditionAsync repeatedly evaluates a check function with configurable wait strategy between attempts; each iteration is its own Lambda invocation (suspended via STEP+RETRY checkpoints carrying NextAttemptDelaySeconds), so polling does not consume compute time.

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

Fixes DOTNET-8665.

Public surface

  • IDurableContext.WaitForConditionAsync<TState> (reflection + AOT-safe overloads taking ICheckpointSerializer<TState>)
  • IConditionCheckContext (Logger + AttemptNumber)
  • WaitForConditionConfig<TState> (required InitialState + WaitStrategy)
  • IWaitStrategy<TState> with Decide(state, attempt) returning WaitDecision
  • WaitDecision (readonly record struct; ShouldContinue + Delay; Stop() / ContinueAfter(TimeSpan) factories)
  • WaitStrategy factories: Exponential / Linear / Fixed / FromDelegate, each accepting an optional Func<TState, bool> isDone predicate
  • WaitForConditionException with AttemptsExhausted and LastState (preserved across both live execution and replay)

Internal

  • WaitForConditionOperation<TState> wire format = STEP + SubType "WaitForCondition". Each polling iteration emits Action=RETRY with the new state in payload and NextAttemptDelaySeconds for the service to schedule the next invocation.
  • Strategies signal max-attempts exhausted by throwing WaitForConditionException directly from Decide(); the operation enriches with LastState before checkpointing FAIL.
  • LastState survives FAIL replay: serialized into FAIL payload at write time, deserialized in BuildFailureException with warning-logged fallback for legacy/corrupt data.
  • ExponentialBackoff helper extracted for sharing with ExponentialRetryStrategy. Math is byte-for-byte identical.
  • Reuses OperationSubTypes.WaitForCondition from Wave 0.

Defaults

60 attempts / 5s initial / 300s max / 1.5x rate / Full jitter — distinct from RetryStrategy.Default and matching Python/JS/Java reference SDKs.

Cross-SDK note: Python returns success on max-attempts exhausted; .NET/Java/JS throw. Workflows ported from Python should review for new failure modes. Documented in the design doc.

Test plan

  • Build clean (zero warnings, TreatWarningsAsErrors enforced) on net8.0 and net10.0
  • 41 new unit tests pass alongside existing 165, including each wait strategy, isDone predicate paths, max-attempts exhaustion, user-check exceptions, replay determinism, exponential backoff bounds, and corrupt-payload fallback logging
  • 5 new integration tests build successfully (require AWS credentials to run)

🤖 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-waitforcondition branch from 87688ca to 7f91202 Compare May 14, 2026 21:49
Adds service-mediated polling to the .NET Durable Execution SDK.
WaitForConditionAsync repeatedly evaluates a check function with
configurable wait strategy between attempts; each iteration is its own
Lambda invocation (suspended via STEP+RETRY checkpoints carrying
NextAttemptDelaySeconds), so polling does not consume compute time.

Public surface:
- IDurableContext.WaitForConditionAsync<TState> (reflection + AOT-safe
  overloads taking ICheckpointSerializer<TState>)
- IConditionCheckContext (Logger + AttemptNumber)
- WaitForConditionConfig<TState> (required InitialState + WaitStrategy)
- IWaitStrategy<TState> with Decide(state, attempt) returning
  WaitDecision
- WaitDecision (readonly record struct, ShouldContinue + Delay,
  Stop() / ContinueAfter(TimeSpan) factories)
- WaitStrategy factories: Exponential / Linear / Fixed / FromDelegate,
  each accepting an optional Func<TState, bool> isDone predicate
- WaitForConditionException with AttemptsExhausted and LastState
  (preserved across both live execution and replay)

Internal:
- WaitForConditionOperation<TState> wire format = STEP + SubType
  "WaitForCondition". Each polling iteration emits Action=RETRY with
  the new state in payload and NextAttemptDelaySeconds for the
  service to schedule the next invocation.
- Strategies signal max-attempts exhausted by throwing
  WaitForConditionException directly from Decide(); the operation
  enriches with LastState before checkpointing FAIL.
- LastState survives FAIL replay: serialized into FAIL payload at
  write time, deserialized in BuildFailureException with
  warning-logged fallback for legacy/corrupt data.
- ExponentialBackoff helper extracted for sharing with
  ExponentialRetryStrategy. Math is byte-for-byte identical.
- Reuses OperationSubTypes.WaitForCondition from Wave 0.

Defaults: 60 attempts / 5s initial / 300s max / 1.5x rate / Full jitter -
distinct from RetryStrategy.Default and matching Python/JS/Java reference
SDKs. (Note: Python returns success on max-attempts; .NET/Java/JS throw
- documented in design doc.)

Adds 41 unit tests + 5 integration tests covering each wait strategy,
isDone predicate paths, max-attempts exhaustion, user-check exceptions,
replay determinism, exponential backoff bounds, and corrupt-payload
fallback logging.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@GarrettBeatty GarrettBeatty force-pushed the gcbeatty/durable-waitforcondition branch from 7f91202 to 3fa06ce Compare May 14, 2026 22:19
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