-
Notifications
You must be signed in to change notification settings - Fork 4
Keep startup workflow binds recoverable #3172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/integrate
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,8 @@ internal sealed class WorkflowDefinitionBootstrapHostedService : IHostedService | |
| private readonly FileBackedWorkflowCatalogPort _definitionMaterializer; | ||
| private readonly IOptions<WorkflowDefinitionFileSourceOptions> _options; | ||
| private readonly ILogger<WorkflowDefinitionBootstrapHostedService> _logger; | ||
| private CancellationTokenSource? _retryCts; | ||
| private Task? _retryTask; | ||
|
|
||
| public WorkflowDefinitionBootstrapHostedService( | ||
| IWorkflowDefinitionCatalog registry, | ||
|
|
@@ -40,8 +42,127 @@ public async Task StartAsync(CancellationToken cancellationToken) | |
| .Where(definition => definition != null) | ||
| .Select(definition => definition!) | ||
| .ToList(); | ||
| await _definitionMaterializer.MaterializeAsync(definitions, cancellationToken); | ||
| try | ||
| { | ||
| await _definitionMaterializer.MaterializeAsync(definitions, cancellationToken); | ||
| } | ||
| catch (WorkflowDefinitionMaterializationException ex) | ||
| when (ex.Code == WorkflowDefinitionMaterializationException.BindNotCommittedCode && | ||
| !cancellationToken.IsCancellationRequested) | ||
| { | ||
| _logger.LogWarning( | ||
| ex, | ||
| "Startup workflow definition bind was not observed before timeout; host startup will continue and retry materialization in the background. workflow_name={WorkflowName} actor_id={ActorId} expected_execution_mode={ExpectedExecutionMode}", | ||
| ex.WorkflowName, | ||
| ex.ActorId, | ||
| ex.ExpectedExecutionMode); | ||
| StartBackgroundRetry(definitions, ex.WorkflowName); | ||
| } | ||
| } | ||
|
|
||
| public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; | ||
| public async Task StopAsync(CancellationToken cancellationToken) | ||
| { | ||
| if (_retryCts == null || _retryTask == null) | ||
| return; | ||
|
|
||
| await _retryCts.CancelAsync(); | ||
| try | ||
| { | ||
| await _retryTask.WaitAsync(cancellationToken); | ||
| } | ||
| catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) | ||
| { | ||
| throw; | ||
| } | ||
| catch (OperationCanceledException) | ||
| { | ||
| } | ||
| finally | ||
| { | ||
| _retryCts.Dispose(); | ||
| _retryCts = null; | ||
| _retryTask = null; | ||
| } | ||
| } | ||
|
|
||
| private void StartBackgroundRetry( | ||
| IReadOnlyList<WorkflowDefinitionRegistration> definitions, | ||
| string timedOutWorkflowName) | ||
| { | ||
| var retryDelay = _options.Value.BindCommitRetryDelay; | ||
| if (retryDelay <= TimeSpan.Zero) | ||
| { | ||
| throw new ArgumentOutOfRangeException( | ||
| nameof(WorkflowDefinitionFileSourceOptions.BindCommitRetryDelay), | ||
| retryDelay, | ||
| "Workflow definition bind commit retry delay must be positive."); | ||
| } | ||
|
|
||
| var remainingDefinitions = GetDefinitionsAfter(definitions, timedOutWorkflowName); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an accepted dispatch never commits, AGENTS.md reference: AGENTS.md:L57-L57 Useful? React with 馃憤聽/ 馃憥. |
||
| if (remainingDefinitions.Count == 0) | ||
| { | ||
| _logger.LogInformation( | ||
| "Startup workflow definition materialization has no remaining definitions after timed-out bind observation. workflow_name={WorkflowName}", | ||
| timedOutWorkflowName); | ||
| return; | ||
| } | ||
|
|
||
| _retryCts = new CancellationTokenSource(); | ||
| _retryTask = RetryMaterializationAsync(remainingDefinitions, retryDelay, _retryCts.Token); | ||
| } | ||
|
|
||
| private async Task RetryMaterializationAsync( | ||
| IReadOnlyList<WorkflowDefinitionRegistration> definitions, | ||
| TimeSpan retryDelay, | ||
| CancellationToken ct) | ||
| { | ||
| var remainingDefinitions = definitions; | ||
| while (!ct.IsCancellationRequested && remainingDefinitions.Count > 0) | ||
| { | ||
| try | ||
| { | ||
| await Task.Delay(retryDelay, ct); | ||
| await _definitionMaterializer.MaterializeAsync(remainingDefinitions, ct); | ||
| _logger.LogInformation("Startup workflow definition materialization completed in background retry."); | ||
| return; | ||
| } | ||
| catch (OperationCanceledException) when (ct.IsCancellationRequested) | ||
| { | ||
| return; | ||
| } | ||
| catch (WorkflowDefinitionMaterializationException ex) | ||
| when (ex.Code == WorkflowDefinitionMaterializationException.BindNotCommittedCode) | ||
| { | ||
| _logger.LogWarning( | ||
| ex, | ||
| "Startup workflow definition bind still has not been observed; materialization will continue with remaining definitions. workflow_name={WorkflowName} actor_id={ActorId} expected_execution_mode={ExpectedExecutionMode}", | ||
| ex.WorkflowName, | ||
| ex.ActorId, | ||
| ex.ExpectedExecutionMode); | ||
| remainingDefinitions = GetDefinitionsAfter(remainingDefinitions, ex.WorkflowName); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Startup workflow definition materialization retry failed; materialization will retry."); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static IReadOnlyList<WorkflowDefinitionRegistration> GetDefinitionsAfter( | ||
| IReadOnlyList<WorkflowDefinitionRegistration> definitions, | ||
| string workflowName) | ||
| { | ||
| var orderedDefinitions = definitions | ||
| .OrderBy(definition => definition.WorkflowName, StringComparer.OrdinalIgnoreCase) | ||
| .ToArray(); | ||
| var failedIndex = Array.FindIndex( | ||
| orderedDefinitions, | ||
| definition => string.Equals( | ||
| definition.WorkflowName, | ||
| workflowName, | ||
| StringComparison.OrdinalIgnoreCase)); | ||
| return failedIndex < 0 || failedIndex + 1 >= orderedDefinitions.Length | ||
| ? [] | ||
| : orderedDefinitions[(failedIndex + 1)..]; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the host startup token and the private bind timeout are both requested,
WaitForCommittedBindAsyncnow converts the cancellation intoBindNotCommitted, but this filter immediately rejects that exception because the startup token is canceled. The exception therefore still escapesStartAsyncand aborts host startup in the exact race this change is intended to recover; the added test usesCancellationToken.Noneand does not exercise this path.Useful? React with 馃憤聽/ 馃憥.