{"record":{"id":"6c59523623e4df48","repo":"elsa-workflows/elsa-core","slug":"drain-already-completed-in-this-generation-subsequent-non","errorCode":null,"errorMessage":"Drain already completed in this generation; subsequent non-force invocations are rejected.","messagePattern":"Drain already completed in this generation; subsequent non-force invocations are rejected\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"warning","filePath":"src/modules/Elsa.Workflows.Runtime/Services/DrainOrchestrator.cs","lineNumber":91,"sourceCode":"        _identityGenerator = identityGenerator;\n        _logger = logger;\n    }\n\n    /// <inheritdoc />\n    public async ValueTask<DrainOutcome> DrainAsync(DrainTrigger trigger, CancellationToken cancellationToken = default)\n    {\n        lock (_sync)\n        {\n            if (_drainInProgress)\n            {\n                if (trigger == DrainTrigger.OperatorForce && _previousOutcome is not null)\n                    return _previousOutcome with { WasCached = true };\n                throw new InvalidOperationException($\"Drain already in progress; second invocation rejected (trigger={trigger}).\");\n            }\n            if (_previousOutcome is not null)\n            {\n                if (trigger == DrainTrigger.OperatorForce) return _previousOutcome with { WasCached = true };\n                throw new InvalidOperationException(\"Drain already completed in this generation; subsequent non-force invocations are rejected.\");\n            }\n            _drainInProgress = true;\n        }\n\n        var startedAt = _clock.UtcNow;\n        var deadline = ComputeEffectiveDeadline(trigger);\n        var sw = Stopwatch.StartNew();\n        TimeSpan pausePhase = TimeSpan.Zero;\n        TimeSpan waitPhase = TimeSpan.Zero;\n\n        try\n        {\n            await _signal.BeginDrainAsync(cancellationToken);\n            _logger.LogInformation(\"Drain initiated (trigger={Trigger}, deadline={Deadline}).\", trigger, deadline);\n\n            var deadlineAt = startedAt + deadline;\n\n            // Phase 1: parallel pause. Each source has its own timeout independent of the overall deadline,","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.Workflows.Runtime/Services/DrainOrchestrator.cs#L73-L109","documentation":"DrainOrchestrator caches the outcome of a completed drain per generation. Any subsequent DrainAsync call without DrainTrigger.OperatorForce after a drain already completed is rejected with InvalidOperationException. Operator-force invocations get the cached outcome (WasCached = true) instead.","triggerScenarios":"Calling DrainAsync a second time after the first drain finished, without DrainTrigger.OperatorForce — e.g. repeated shutdown hooks or periodic jobs firing after the generation's drain already ran.","commonSituations":"Application shutdown handlers registered twice; a retry policy re-running a drain that already succeeded; tests invoking DrainAsync multiple times against a shared orchestrator instance without resetting generation.","solutions":["Track whether the drain already ran for this generation and skip the second call.","Use DrainTrigger.OperatorForce when a re-invocation is intentional so the cached outcome is returned.","Create/reset the orchestrator (new generation) if a fresh drain is genuinely required.","Catch InvalidOperationException and read the previous outcome instead of failing."],"exampleFix":"// before\n// called on every shutdown hook\nawait drainOrchestrator.DrainAsync(DrainTrigger.Shutdown, ct);\n\n// after\nif (!_drainCompleted)\n{\n    var outcome = await drainOrchestrator.DrainAsync(DrainTrigger.Shutdown, ct);\n    _drainCompleted = true;\n}\nelse\n{\n    var outcome = await drainOrchestrator.DrainAsync(DrainTrigger.OperatorForce, ct); // cached\n}","handlingStrategy":"try-catch","validationCode":"// Track locally: if a drain already completed this generation, use OperatorForce or skip.","typeGuard":null,"tryCatchPattern":"try\n{\n    await drainOrchestrator.DrainAsync(DrainTrigger.Shutdown, ct);\n}\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"Drain already completed\"))\n{\n    var cached = await drainOrchestrator.DrainAsync(DrainTrigger.OperatorForce, ct);\n}","preventionTips":["Track whether a drain already ran per generation","Use OperatorForce for intentional re-invocations","Avoid repeated shutdown hooks invoking the drain"],"tags":["concurrency","drain","runtime","invalid-operation"],"backgroundTag":"invalid-state-transition","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}