elsa-workflows / elsa-workflows/elsa-core

PublishEvent / BackgroundStimulusDispatcher bypasses transactional workflow-dispatch outbox

Closed
#8,150 0 comments 0 reactions 0 assignees View on GitHub
bug core elsa 3 prio high tech debt triaged
Dominant language
C#
Stars
7.9k
Forks
1.5k
Avg merge
15h 22m
Merged PRs (30d)
114

Description

## Problem

When `WorkflowDispatcherOptions.UseTransactionalOutbox` is enabled, **child workflow dispatches** made during an execution cycle are durable (written to the outbox and delivered after parent state commits). **Published events from the same cycle are not** — `PublishEvent` always goes through `IStimulusDispatcher` / `BackgroundStimulusDispatcher`, which enqueues a mediator background command immediately and never touches `TransactionalWorkflowDispatcher` / `IWorkflowDispatchOutbox`.

That is a silent durability gap: hosts that turn on the outbox for crash-safety still lose in-workflow events if the process dies between activity complete and background command processing.

## Evidence (main)

### PublishEvent always picks the thin stimulus path

`src/modules/Elsa.Workflows.Runtime/Activities/PublishEvent.cs`:

```csharp
await publisher.PublishAsync(eventName, correlationId, workflowInstanceId, null, payload, true, context.CancellationToken);
// ^^^^ asynchronous: true
```

`EventPublisher` with `asynchronous: true` calls `IStimulusDispatcher.SendAsync`, not `IWorkflowDispatcher`.

### BackgroundStimulusDispatcher — no outbox, drop caller CT

`BackgroundStimulusDispatcher.SendAsync` → `commandSender.SendAsync(DispatchStimulusCommand, CommandStrategy.Background, …, CancellationToken.None)` and returns `DispatchStimulusResponse.Empty`.

Default DI registers only that implementation for `IStimulusDispatcher` (no transactional decorator).

### Contrast — Trigger/Resume on IWorkflowDispatcher *do* get outbox

`TransactionalWorkflowDispatcher.DispatchAsync(DispatchTriggerWorkflowsRequest|DispatchResumeWorkflowsRequest)` enqueues `WorkflowDispatchOutboxItemKind.TriggerWorkflows` / `ResumeWorkflows` when inside a `WorkflowExecutionContext` and `UseTransactionalOutbox` is true — then the same eventual `IStimulusSender.SendAsync` runs after commit.

So the **durable** async stimulus path already exists on `IWorkflowDispatcher`; `PublishEvent` simply does not use it.

### Same destination, different reliability

`DispatchStimulusCommandHandler` and `DispatchWorkflowCommandHandler` (Trigger/Resume) both end at `IStimulusSender.SendAsync`. Only the entry façade differs — and with it, crash semantics.

## Expected

- In-workflow async stimulus (at least `PublishEvent`) should honor the same transactional-outbox contract as in-workflow workflow dispatch when the option is enabled — **or** docs/options must state that outbox covers workflow dispatch only, not events.
- Prefer subtractive fix: route `PublishEvent` / async `EventPublisher` through the durable façade (see #8149), rather than building a second outbox for `IStimulusDispatcher`.

## Suggested follow-through

1. Decide: events are in scope for `UseTransactionalOutbox` (recommended for consistency) or explicitly out of scope (document + warn on `PublishEvent`).
2. If in scope: change `EventPublisher` async path to `IWorkflowDispatcher.DispatchAsync(new DispatchTriggerWorkflowsRequest(...))` (or a single collapsed async stimulus API from #8149) so `TransactionalWorkflowDispatcher` applies.
3. Conformance/integration test: with outbox on, `PublishEvent` inside a parent workflow must not lose the stimulus across a simulated crash between parent commit and delivery (mirror existing workflow-dispatch outbox tests).

## Not a duplicate of

- #8149 — dual/triple stimulus entry surface (root API smell; this issue is the concrete durability bug)
- #8136 — obsolete inbox DI
- #8135 — unmatched-resume enqueue field drops
- #8042 — activation strategies on dispatched definitions
- Wiki outbox notes for `TransactionalWorkflowDispatcher` — describe workflow dispatch only today

## Milestone

Unset — Elsa 3 Issue Triage / Crew Lead place. Soft proposal: Ready-after-prune or Backlog with prio once confirmed against production outbox users; not auto 3.9. Do not auto-assign Engineer.

Contributor guide

Open the contributing guide

Research direction

Start with src/modules/Elsa.Workflows.Runtime/Activities/PublishEvent.cs, then trace EventPublisher, BackgroundStimulusDispatcher, and TransactionalWorkflowDispatcher. Compare the existing workflow-dispatch outbox tests and confirm how DispatchStimulusCommand reaches IStimulusSender. Done when the outbox scope is decided and either crash-safe PublishEvent coverage exists or the limitation is documented and warned about.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
backend, backend-api-design
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.