Repository navigation
Add the in-process event bus and the outbox - #2755
Merged
Merged
Conversation
Events raised in the content service's transaction are held until the outermost commit and dropped on rollback; outbox handlers get their row in the same transaction and a drain job delivers it at least once. Closes #2697
3 tasks
Stage the outbox rows without saving what the caller tracked and also inside a foreign transaction; probe both contexts; renew the lease before each delivery and cut a delivery off before its lease ends; give the drain its own queue; record job events with the job outcome; stamp the source of a publish; run handlers under the operation of the event; order rows by a sequence; scrub stored errors; add a service for failed deliveries.
…eview round Claim without using an attempt; run handlers on their own thread so a blocking one can be abandoned; keep a nested write from re-staging or losing the buffered events; bound the wait of the job runner for in-process job event handlers; scrub secrets from job event errors and stored errors; give job events the site of the job.
This was referenced Nov 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2697
What
IEventBus,IEventHandler<TEvent>andIOutboxEventHandler<TEvent>in the SDK, with the event records of spec 4.11 that this issue names: the ten entry events,SettingsChangedEvent,SiteCreatedEvent,SiteUpdatedEvent,CacheInvalidatedEvent, andJobFailedEvent/JobDeadLetteredEvent, moved to the SDK.CmsEventcarries an id and anEventSource(site, audited actor and its kind, origin, correlation id). The bus fillsSourcefrom the current operation when the publisher left it out.IEntryChangeSinkis implemented byEntryChangeSink: it hears a change only after the outermost commit and maps it to an event.IEntryChangeStagerlets it write the outbox rows inside the transaction, just before the commit.IEventBuswhile the content service holds its transaction are buffered (deduplicated byEventId; a handler that writes during dispatch gets a buffer of its own and can neither repeat nor lose them) and dropped when the transaction or a savepoint rolls back. Outside any transaction they are delivered at once. Inside a transaction onCmsDbContextorSystemDbContextthat the service did not begin, publishing throwsFCMS2602, since its end cannot be seen. There is no API yet to stage rows inside aSystemDbContexttransaction; Typed settings model persisted per site #2704 can add one when it needs it.cms_outbox: one row per event and outbox handler, written in the same transaction, saved without saving anything else the caller tracked. A write inside a transaction the service did not begin also writes its rows inside it, so they commit or roll back with it; the handlers of this process still hear nothing of such a write, as in Add the content service with optimistic concurrency #2747.OutboxDrainerclaims a batch with a conditional update and a lease. Claiming costs no attempt: the worker starts an attempt, and takes its lease again, with one conditional update before each delivery, which matches nothing once another instance has the row, so a worker whose lease was taken over delivers nothing more and a row whose handler never ran is never failed. A delivery runs on a thread of its own and is cut off afterDeliveryTimeout(1 minute, shorter than the 5-minute lease), logged as abandoned and counted as a failed attempt; an abandoned handler may still be running, since nothing can stop it. A failed delivery is retried with backoff and left asfailedafterMaxAttempts; a row whose started attempts are used up by workers that died is failed at the next claim; a row whose handler this instance lacks waits without using an attempt. The idempotency key is the row id.outboxqueue (one worker by default), so a hung handler holds up no other job. It is asked for after a commit that recorded rows, and a recurring run each minute (which also prunes delivered rows) catches the rest. The recurring run is registered only when an outbox handler exists, and retried until it is stored.JobStorewrites the outbox rows ofJobFailedEvent/JobDeadLetteredEventin the same transaction as the job's outcome, with ids derived from the job and the attempt, and the site of the job as the site of the event.BusJobEventSinkreplacesNullJobEventSinkand runs the handlers of this process after the commit, under a system operation on the site of the job and in its default locale. A job worker waits for them at mostJobEventTimeout(5 seconds); handlers that take longer are left running on a thread of their own and logged.key=valuepair whose key names a secret (token,secret,password,key,authorization,signatureand similar). A secret in any other shape is not recognised.cms_job.last_erroris not changed by this PR.IEventDeadLetterslists, retries and removes failed deliveries.AddOutboxfor SQLite, PostgreSQL and SQL Server, perdocs/system-migrations.md. No seed data.cms.Events.AddHandlerandcms.Events.AddOutboxHandler(name)register handlers. They run in[HookOrder]order, then by theIHookTiebreakerrank, then by registration.Acceptance
[HookOrder]and plugin dependency order, for in-process handlers and for the start of outbox deliveries. The plugin host does not exist yet, so the dependency order is the existingIHookTiebreakerseam, tested with a stub. Outbox delivery order across instances and across retries is not guaranteed, as the SDK documentation says; the order of the events of one commit and of one entry holds from one instance.Tests of the failure paths
A handler that throws or cancels, or cannot be built, leaves the write and the other events alone; a crash between commit and dispatch; a commit that fails; a stage that fails and leaves nothing for the next write of the scope; a settle that fails; a lease that is taken over under a live worker; three instances draining together; a worker that dies on every attempt; a handler that never returns; a drain that is cancelled mid-batch; a handler the instance does not have; a job outcome whose event cannot be recorded; a handler that leaves a poisoned entity in the shared context; a handler that writes content while it is told.
Notes
OutboxWriterandEventBususeDbContextfrom the service or a probe inCore.Content, because the architecture tests allow only the content and storage layers to touchCmsDbContext.Dependencies
No new packages.