Repository navigation
SinkWorker: one queue per channel, sized by the pool - #16
Merged
Merged
Conversation
This was referenced Oct 7, 2026
facontidavide
pushed a commit
that referenced
this pull request
Oct 7, 2026
…emselves Every channel published into one moodycamel queue per SinkWorker; explicit producers keep the blocks they take, so a channel could be rejected while its pool still had free slots, and users had to size queue_capacity by hand. Each (channel, sink) attachment now has a single-producer single-consumer ring of exactly the pool's capacity: the pool is the only bound. The worker polls 20 us, then sleeps on an eventcount with a std::binary_semaphore; a push costs one increment and one load and wakes the worker only after it slept. - Removed: queue_capacity, kDefaultQueueCapacity, the moodycamel names in data_sink.hpp and the vendored 3rdparty/ConcurrentQueue, LogChannel::getActiveFlags(), the public SnapshotRef(pool, slot). - LogChannel::setPoolCapacity(stall, period); exact payload reserve for fixed-size schemas; addDataSink() returns whether it attached. - DataSink::onStop() and onStart(): sinks close files, write pending dumps and flush batches when the worker stops, and MCAPSink reopens on start. The DataSink and CustomSerializer vtables are frozen for 2.x. - ChannelsRegistry: stopAll(), setChannelDefaults(ChannelDefaults), addDefaultSink() also reaches existing channels (undone on refusal). - SinkWorker and LogChannel keep their sink and type registry in the Pimpl; abi_tests pins the layouts. - README, LLM manual, CLAUDE.md, header comments and CHANGELOG updated. Reviewed twice (memory ordering, wake-ups, attach/detach, lifecycle) and simplified; see PR #16 and #17. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WASypwgBSXDzJkPN4HBtFD
facontidavide
force-pushed
the
spsc-sink-queue
branch
from
October 7, 2026 07:57
f3b3dad to
4c8045f
Compare
…emselves Every channel published into one moodycamel queue per SinkWorker; explicit producers keep the blocks they take, so a channel could be rejected while its pool still had free slots, and users had to size queue_capacity by hand. Each (channel, sink) attachment now has a single-producer single-consumer ring of exactly the pool's capacity: the pool is the only bound. The worker polls 20 us, then sleeps on an eventcount with a std::binary_semaphore; a push costs one increment and one load and wakes the worker only after it slept. - Removed: queue_capacity, kDefaultQueueCapacity, the moodycamel names in data_sink.hpp and the vendored 3rdparty/ConcurrentQueue, LogChannel::getActiveFlags(), the public SnapshotRef(pool, slot). - LogChannel::setPoolCapacity(stall, period); exact payload reserve for fixed-size schemas; addDataSink() returns whether it attached. - DataSink::onStop() and onStart(): sinks close files, write pending dumps and flush batches when the worker stops, and MCAPSink reopens on start. The DataSink and CustomSerializer vtables are frozen for 2.x. - ChannelsRegistry: stopAll(), setChannelDefaults(ChannelDefaults), addDefaultSink() also reaches existing channels (undone on refusal). - SinkWorker and LogChannel keep their sink and type registry in the Pimpl; abi_tests pins the layouts. - README, LLM manual, CLAUDE.md, header comments and CHANGELOG updated. Reviewed twice (memory ordering, wake-ups, attach/detach, lifecycle) and simplified; see PR #16 and #17. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WASypwgBSXDzJkPN4HBtFD
facontidavide
force-pushed
the
spsc-sink-queue
branch
from
October 7, 2026 08:06
4c8045f to
15bbe81
Compare
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.
Replaces the shared sink queue with one single-producer single-consumer ring per (channel, sink) attachment. Each ring has exactly the channel's pool capacity, so a snapshot can no longer be refused while its pool has a free slot. The pool becomes the only bound, and queue sizing disappears from the API.
Why
Every channel published into one moodycamel queue per
SinkWorker. An explicit producer keeps the 32-entry blocks it takes, so with the default 1024 entries a channel could getrejectedwhile its pool still had free slots. Users had to sizequeue_capacityfrom channel counts and block arithmetic.What changes
prepare(), or inaddDataSink()on a prepared channel.std::binary_semaphore.std::atomic::waitis avoided because, in libstdc++, it can turn the existing per-snapshotnotify_all()calls into futex wakes.SinkWorker::kDefaultQueueCapacityand thequeue_capacityargument.data_sink.hpp.3rdparty/ConcurrentQueue, which nothing uses any more.LogChannel::getActiveFlags().SnapshotRef(pool, slot)constructor. It's now private and built bySnapshotPool::adopt().setPoolCapacity(stall_tolerance, snapshot_period)that computesceil(stall / period)slots. It's validated and overflow-safe.ValuePtr::isFixedSize()), each slot reserves exactly the payload. Otherwise it keepsmax(2x, 256).setPayloadCapacity()stays a floor.SinkWorkerkeeps its sink in the Pimpl andLogChannelkeeps itsTypesRegistryin the Pimpl.abi_testspins both sizes. 2.0.0 is not tagged yet.Real-time path
No new allocation, lock or I/O.
Futex calls on the RT thread, measured with strace on a laptop over 2000 ticks:
The poll costs up to 2 % of a core per worker at 1 kHz. This hasn't been measured on an embedded target yet.
Review
An independent review covered:
It found 2 important issues. Both are fixed:
Testing
ChannelCapacity.TenThousand*, x50New tests:
drain()frees detached rings;🤖 Generated with Claude Code
https://claude.ai/code/session_01WASypwgBSXDzJkPN4HBtFD