{"record":{"id":"5046341d2061491e","repo":"elsa-workflows/elsa-core","slug":"drain-already-in-progress-second-invocation-rejected-trigger","errorCode":null,"errorMessage":"Drain already in progress; second invocation rejected (trigger={trigger}).","messagePattern":"Drain already in progress; second invocation rejected \\(trigger=(.+?)\\)\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"warning","filePath":"src/modules/Elsa.Workflows.Runtime/Services/DrainOrchestrator.cs","lineNumber":86,"sourceCode":"        _cycles = cycles;\n        _scopeFactory = scopeFactory;\n        _options = options;\n        _hostOptions = hostOptions;\n        _clock = clock;\n        _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);","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.Workflows.Runtime/Services/DrainOrchestrator.cs#L68-L104","documentation":"DrainOrchestrator.DrainAsync serializes drain operations with a lock and a _drainInProgress flag. When a drain is already running, a second invocation is rejected with InvalidOperationException, except when the caller passes DrainTrigger.OperatorForce and a previous outcome exists, in which case the cached outcome is returned. This prevents concurrent drain passes from racing.","triggerScenarios":"Invoking DrainAsync (public) while another DrainAsync call is still executing (e.g. two operators or a scheduler overlapping an in-flight drain) without DrainTrigger.OperatorForce.","commonSituations":"A scheduled drain overlapping a manual operator-triggered drain; retry logic re-invoking DrainAsync while the first attempt is still running; multiple instances of an admin job sharing the same orchestrator singleton.","solutions":["Wait for the in-flight drain to complete before invoking again; check the orchestrator's state/outcome first.","If intentional, pass DrainTrigger.OperatorForce to obtain the cached previous outcome instead of throwing.","Ensure DrainAsync is invoked from a single place (singleton coordinator) rather than concurrently from multiple schedulers.","Wrap the call in try/catch for InvalidOperationException and treat it as 'drain already running' rather than a fault."],"exampleFix":"// before\nawait drainOrchestrator.DrainAsync(DrainTrigger.Shutdown, ct);\n\n// after\ntry\n{\n    await drainOrchestrator.DrainAsync(DrainTrigger.Shutdown, ct);\n}\ncatch (InvalidOperationException) when (ex.Message.Contains(\"Drain already in progress\"))\n{\n    logger.LogInformation(\"Drain already in progress; skipping this invocation.\");\n}","handlingStrategy":"try-catch","validationCode":"// No pre-call API to check in-flight state; rely on single-caller design.","typeGuard":null,"tryCatchPattern":"try\n{\n    await drainOrchestrator.DrainAsync(trigger, ct);\n}\ncatch (InvalidOperationException ex) when (ex.Message.StartsWith(\"Drain already in progress\"))\n{\n    logger.LogInformation(\"Drain skipped: already running.\");\n}","preventionTips":["Invoke DrainAsync from a single coordinator","Never overlap scheduled and manual drains","Use OperatorForce when a cached outcome is acceptable"],"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-15T23:17:13.987Z"}