Scale out error ingestion for EF (2/3): --error-ingestion-only host - #5801
Merged
Conversation
johnsimons
force-pushed
the
john/scaleout_error2
branch
from
August 19, 2026 22:31
90fd965 to
ce6d746
Compare
johnsimons
force-pushed
the
john/scaleout_error2
branch
from
August 20, 2026 02:57
72d6cc8 to
a03aae7
Compare
Introduces the `--error-ingestion-only` command to allow multiple ServiceControl processes to share the load of draining error queues into a single database. This mode disables singleton background tasks—such as retention sweeps, the retry pipeline, and heartbeat monitoring—to ensure they only run on the primary instance. Support is currently limited to SQL Server and PostgreSQL storage.
johnsimons
force-pushed
the
john/scaleout_error2
branch
from
August 20, 2026 04:15
a03aae7 to
5853be3
Compare
rbev
approved these changes
Aug 20, 2026
Comment on lines
+77
to
+81
| [Test] | ||
| public void Should_refuse_to_start_against_unsupported_storage() | ||
| { | ||
| var settings = new Settings(TransportIntegration.TypeName, "RavenDB", CreateLoggingSettings(), | ||
| forwardErrorMessages: false, errorRetentionPeriod: TimeSpan.FromDays(10)); |
Contributor
There was a problem hiding this comment.
I know it's the only case right now so this isn't strictly necessary, but the test name is generic as if it should be testing all unsupported types.
Suggested change
| [Test] | |
| public void Should_refuse_to_start_against_unsupported_storage() | |
| { | |
| var settings = new Settings(TransportIntegration.TypeName, "RavenDB", CreateLoggingSettings(), | |
| forwardErrorMessages: false, errorRetentionPeriod: TimeSpan.FromDays(10)); | |
| [TestCase("RavenDB")] | |
| public void Should_refuse_to_start_against_unsupported_storage(string storageType) | |
| { | |
| var settings = new Settings(TransportIntegration.TypeName, storageType, CreateLoggingSettings(), | |
| forwardErrorMessages: false, errorRetentionPeriod: TimeSpan.FromDays(10)); |
| hostBuilder.Services.AddEventLogMapping<CustomCheckSucceededDefinition>(); | ||
| hostBuilder.Services.AddPlatformConnectionProvider<CustomChecksPlatformConnectionDetailsProvider>(); | ||
|
|
||
| if (!settings.ErrorIngestionOnly) |
Contributor
There was a problem hiding this comment.
Could this config be future-proofed as an INSTANCE_ROLE or INSTANCE_FEATURES setting that can enable one or more different features?
That way a future instance that has many of these components don't have a web of mutually exclusive flags to configure. (and for us to document)
Member
Author
There was a problem hiding this comment.
we can, i will raise another card for us to think about it
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.
Part 2 of 3 introducing scale-out of error ingestion for EF persistence. Depends on #<PR 1>.
Adds
--error-ingestion-only, which runs a host that does nothing but drain the errorqueue into the shared database. Several of these can run alongside one normal instance.
SQL Server and PostgreSQL only; the command refuses to start on any other storage.
Why this works at all
Ingestion never uses the NServiceBus endpoint.
ErrorIngestionbuilds its ownTransportInfrastructureand receiver through the low-level transport API, which is whycompeting consumers come for free, and after part 1 it forwards through that same
infrastructure's dispatcher. Everything else the ingestion path does after the receive is a
database write: event log, integration dispatch requests, retry claim cleanup, known
endpoints, failed import records.
The batch writer was already built for concurrent writers (
INSERT ... ON CONFLICT/MERGE WITH (HOLDLOCK), newer-wins guards, ordering byUniqueMessageIdfor consistentlock order), and
ErrorIngestionConcurrencyTestsalready covers it.No NServiceBus endpoint
The endpoint is not hosted. Its only remaining role would have been as a registration
source for other code in the process, so the host supplies the two things that actually
needed it:
HostInformation(derived from the machine name, so each node reports its owncustom check row) and
CriticalError(logs and stops the application).A send-only endpoint looks like the safer option but is worse.
SendOnly()dropsReceiveAddressesanyway, which the EFSubscriptionStorageregistered inBasePersistenceneeds, and with the endpoint running the subscription feature activateson any transport without native pub/sub (SQL Server, PostgreSQL, ASQ, MSMQ) and resolves a
store it cannot construct. Dropping the endpoint avoids that entirely.
What is switched off, and why
EventDispatcherHostedServiceReturnToSenderDequeuerRunRetryProcessor, because it is the only writer ofErrorQueueNameCache.ResolvedErrorAddress, whichEditHandlerreads.RetentionSweeperPersistenceSettings.RunRetentionSweep.HeartbeatEndpointSettingsSyncHostedService, heartbeat checkingAddEmailNotifications,AddLicenseCheckCustomChecksMailNotificationneedsIMessageSession, and nothing here readsActiveLicense.ReceiveAddresses. Nothing resolves them today, but leaving them registered is a trap.IntegrationEventWriteris deliberately kept: it performs the database write, and thenormal instance picks the rows up on its polling interval. Without it the event is lost,
not deferred.
Component list
EventLog,ExternalIntegrations,Recoverability,HeartbeatMonitoring,CustomChecks.Not
Hosting(claims the instance queue) orLicensing(would count throughput once pernode).
Four of those five are required, and the reason is worth stating: which node ingests a
given message is arbitrary, so if nodes behave differently then whether a failure produces
an event log entry becomes a coin flip per message. That is worse than either consistent
choice.
Testing
When_hosting_error_ingestion_only, run on SQL Server and PostgreSQL:and asserts the exact set, so adding one anywhere forces a deliberate decision about
whether it is safe to run on every node
IMessageSessionis absentcolumns, the failure groups, the known endpoint row and the event log entry
The end-to-end assertions were verified by removal: dropping
HeartbeatMonitoringComponentor
EventLogComponentstill builds, starts and ingests, and only the derived data goesmissing. Both now fail loudly.
Known gaps
Message bodies must be readable by every host. This mode should not be combined with file
system body storage unless the path is a shared mount. Documented in
Help.txt, not yetenforced or warned about at startup.