feat(router): fan-out V2 PubSub Publish() via FanoutWriter - #940
Closed
dlinsley wants to merge 1 commit into
Closed
Conversation
- Add FanoutWriter: buffered channel, N-worker fan-out for concurrent Publish() calls - Opt-In wire FanoutWriter into router, replacing the single-goroutine Repeater path - Add BenchmarkFanoutWriter alongside existing serial/parallel benchmarks - Worker count defaults to runtime.NumCPU(); overridable via ROUTER_PUBLISH_WORKERS env var or WithPublishWorkers RouterOption - add properties to job spec for doppler to activate FanoutWriter
dlinsley
marked this pull request as draft
July 28, 2026 20:23
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.
Description
Summary
The router's V2 path currently processes envelopes through a single
Repeatergoroutine callingv2PubSub.Publish()serially. EachPublish()call walks the subscriber tree and invokes every matching subscriber'sSetsequentially, so envelopes queue up behind one another even when CPUs are available.This PR introduces
FanoutWriter, which sits between theRepeaterandv2PubSub.Publish()as an opt-in feature. The Repeater feeds envelopes into a small buffered channel (2 slots per worker); N worker goroutines drain the channel and each callsPublish()independently. This allows multiple envelopes to be published concurrently — each worker handles a different envelope, and theirPublish()calls run in parallel across CPUs. The per-envelope subscriber walk itself is unchanged. This can be activated as an opt-in feature by adding env variableROUTER_FANOUT_WRITER_ENABLED=trueConcurrent publishing means envelopes may be delivered to subscribers in a different order than they were received.
loggregator-releasealready makes no guarantees for envelope ordering — the diode buffer is lossy by design, and subscribers are expected to handle out-of-order delivery.Concurrency concerns to be aware of:
go-diodeis safe for many writers and a single readerv2PubSub.Publish()is safe to be called by many go routinesserver.v2.Repeateris still a single go routine but no longer blocked byv2PubSub.Publish()on each Envelope. It now just forwards envelope pointers from the Many-to-one go-diode to a small buffered channel.Changes
fanout_writer.go— newFanoutWritertype withStart/Stoplifecycle, aWritemethod satisfying theWriterfunction signature, and aWithBufferSizeoption.router.go/config.go—FanoutWriteris opt-in via two new env vars:ROUTER_FANOUT_WRITER_ENABLED— enables the feature (default:false)ROUTER_PUBLISH_WORKERS— number of concurrent workers (default:GOMAXPROCS)router_test.go- extended for when FanoutWriter is enabledfanout_writer_test.go— Ginkgo specs covering delivery correctness and cleanStopbehaviour.pubsub_benchmark_test.go— extended with three new benchmarks (BenchmarkDopplerRouterNoOp,BenchmarkDopplerRouterParallelNoOp,BenchmarkDopplerRouterFanout) and a second fixture PubSub usingSleepSetter(1 µstime.Sleepper subscriber) in an attempt to model realistic per-subscriber cost.Benchmark results
20k subscribers, 1 µs cost each,
GOMAXPROCSswept via-cpu 1,2,4,8,16(n=10,benchstat):The original serializes
Publish()calls, so adding CPUs gives no benefit (times plateau around 164–172 µs). WithFanoutWriter, independent envelopes are published in parallel: at 16 CPUs it is ~94× fasterType of change
Testing performed?
Checklist:
mainbranch, or relevant version branchIf you have any questions, or want to get attention for a PR or issue please reach out on the #logging-and-metrics channel in the cloudfoundry slack