Add DURABLE0011: ContinueAsNew warning for unbounded orchestration loops#660
Add DURABLE0011: ContinueAsNew warning for unbounded orchestration loops#660
Conversation
Adds a new Roslyn analyzer (DURABLE0011) that warns when an orchestration contains a while(true) loop with WaitForExternalEvent or CallSubOrchestratorAsync but no ContinueAsNew call. This pattern leads to unbounded history growth that can cause increasingly expensive replays, persistence timeouts, and service-wide instability. The analyzer detects: - while(true) with WaitForExternalEvent and no ContinueAsNew - while(true) with CallSubOrchestratorAsync and no ContinueAsNew Does not flag: - while(true) with only CallActivityAsync (bounded by design) - while(true) that includes ContinueAsNew (correctly bounded) - Non-loop orchestrations Test status: 5 tests pass, 3 positive-detection tests skipped pending test infrastructure update for TaskOrchestrationContext symbol resolution in CommonAssemblies test references.
There was a problem hiding this comment.
Pull request overview
Adds a new Roslyn analyzer rule (DURABLE0011) to warn about orchestrations that use unbounded while(true) loops with history-growing operations (WaitForExternalEvent / CallSubOrchestratorAsync) but do not call ContinueAsNew, helping prevent unbounded history growth and replay degradation in Durable Task orchestrations.
Changes:
- Introduces
ContinueAsNewOrchestrationAnalyzer(DURABLE0011) to detect problematic unbounded orchestration loops. - Adds localized resource strings (title + message) for the new analyzer.
- Adds release tracking metadata and a new test class (with several diagnostic tests currently skipped).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| test/Analyzers.Tests/Orchestration/ContinueAsNewOrchestrationAnalyzerTests.cs | Adds test coverage for DURABLE0011 scenarios (positive cases currently skipped). |
| src/Analyzers/Resources.resx | Adds title/message resource strings for DURABLE0011. |
| src/Analyzers/Orchestration/ContinueAsNewOrchestrationAnalyzer.cs | Implements the DURABLE0011 analyzer logic. |
| src/Analyzers/AnalyzerReleases.Unshipped.md | Registers DURABLE0011 in analyzer release tracking. |
src/Analyzers/Orchestration/ContinueAsNewOrchestrationAnalyzer.cs
Outdated
Show resolved
Hide resolved
src/Analyzers/Orchestration/ContinueAsNewOrchestrationAnalyzer.cs
Outdated
Show resolved
Hide resolved
test/Analyzers.Tests/Orchestration/ContinueAsNewOrchestrationAnalyzerTests.cs
Outdated
Show resolved
Hide resolved
…e, enable tests - Replace syntax-based IdentifierNameSyntax string matching with semantic IOperation analysis using IsEqualTo to verify methods are actually on TaskOrchestrationContext (fixes false positives/negatives) - Add sealed modifier to ContinueAsNewOrchestrationAnalyzer class - Add Initialize() override to validate TaskOrchestrationContext symbol - Enable all 3 previously-skipped positive detection tests with proper diagnostic location markup - All 8 tests pass (previously only 5 passed, 3 were skipped) - Full test suite: 151 passed, 0 skipped, 0 failed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ability Address Copilot review comment: the analyzer checks for the presence of a ContinueAsNew call within the while loop, not reachability via control-flow analysis. Updated the XML summary to match actual behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
src/Analyzers/Orchestration/ContinueAsNewOrchestrationAnalyzer.cs
Outdated
Show resolved
Hide resolved
test/Analyzers.Tests/Orchestration/ContinueAsNewOrchestrationAnalyzerTests.cs
Outdated
Show resolved
Hide resolved
- Update diagnostic message to say 'within that loop' for accuracy - Document helper-method limitation in visitor XML doc - Cache invocations array and add early break when both flags are set - Use VerifyCS.Diagnostic() instead of manual DiagnosticResult ctor Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
test/Analyzers.Tests/Orchestration/ContinueAsNewOrchestrationAnalyzerTests.cs
Outdated
Show resolved
Hide resolved
…ust WaitForExternalEvent Any TaskOrchestrationContext method call (CallActivityAsync, CreateTimer, WaitForExternalEvent, CallSubOrchestratorAsync, etc.) in a while(true) loop without ContinueAsNew causes unbounded history growth. The analyzer now detects any context method invocation, not just a subset. Changes: - Check ContainingType == TaskOrchestrationContext for any method (except ContinueAsNew itself) instead of matching specific method names - Updated resource message to reflect broader detection - Added test for activity-only loop without ContinueAsNew (now reports) - Added test for while(true) with no context calls (no diagnostic) - 152 total tests pass, 0 skipped Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lEvent Address review comment: the SubOrchestrator test also called WaitForExternalEvent, masking potential regressions. Now tests CallSubOrchestratorAsync in isolation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
src/Analyzers/Orchestration/ContinueAsNewOrchestrationAnalyzer.cs
Outdated
Show resolved
Hide resolved
src/Analyzers/Orchestration/ContinueAsNewOrchestrationAnalyzer.cs
Outdated
Show resolved
Hide resolved
test/Analyzers.Tests/Orchestration/ContinueAsNewOrchestrationAnalyzerTests.cs
Show resolved
Hide resolved
test/Analyzers.Tests/Orchestration/ContinueAsNewOrchestrationAnalyzerTests.cs
Show resolved
Hide resolved
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rename test to TaskOrchestratorWhileTrueWithContextCallsNoContinueAsNew since the loop calls both CallActivityAsync and CreateTimer, not just activities - Document the span containment limitation for lambdas/local functions in the visitor XML doc Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace TextSpan.Contains check with IWhileLoopOperation.Body.Descendants() to walk the semantic operation tree. This correctly excludes invocations inside lambdas or local functions declared within the loop body, which have their own operation subtrees and are not directly executed by the loop. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
test/Analyzers.Tests/Orchestration/ContinueAsNewOrchestrationAnalyzerTests.cs
Show resolved
Hide resolved
src/Analyzers/Orchestration/ContinueAsNewOrchestrationAnalyzer.cs
Dismissed
Show dismissed
Hide dismissed
Cover the third orchestration type (OrchestratorFunc via AddOrchestratorFunc) with both positive-detection and no-diagnostic tests, ensuring DURABLE0011 works across all supported orchestration patterns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
helpLinkUri inconsistent with all other analyzers |
Need to update the other analyzers. This is the wrong link |
Summary
Adds a new Roslyn analyzer DURABLE0011 that warns when an orchestration contains a
while(true)loop that calls anyTaskOrchestrationContextmethod but has noContinueAsNewcall within that loop.Problem this addresses
Long-lived orchestrations that run in unbounded loops accumulate history without limit. Every
TaskOrchestrationContextmethod call (e.g.CallActivityAsync,WaitForExternalEvent,CallSubOrchestratorAsync,CreateTimer) adds to the replay history. This leads to increasingly expensive replays and can cause backend persistence failures. This was identified as the root cause of a production crash loop in the Durable Task Scheduler.What is included
ContinueAsNewOrchestrationAnalyzer.cs-- Roslyn analyzer (ID: DURABLE0011)Resources.resxAnalyzerReleases.Unshipped.mdDetection rules
while(true)+ any context method + noContinueAsNewwhile(true)+ContinueAsNewpresentwhile(true)+CallActivityAsync+ noContinueAsNewwhile(true)loopwhile(true)+WaitForExternalEvent+ noContinueAsNewwhile(true)with no context method callswhile(true)+CallSubOrchestratorAsync+ noContinueAsNewImplementation
IOperationanalysis to verify methods are onTaskOrchestrationContext(no false positives from identically-named methods on other types)TaskOrchestrationContextmethod call (exceptContinueAsNewitself) as history-growingInitialize()validatesTaskOrchestrationContextsymbol is available before runningsealedper repo conventionsTesting
Verification