Visitar URL original
Flush sinks and run periodic tasks when manual backend polling drains the queues by MaxFreedomPollard · Pull Request #989 · odygrd/quill · GitHub
Skip to content

Flush sinks and run periodic tasks when manual backend polling drains the queues - #989

Merged
apozzer305 merged 2 commits into
odygrd:masterfrom
MaxFreedomPollard:manual-backend-worker-poll-idle-work
Sep 7, 2026
Merged

apozzer305 merged 2 commits into
odygrd:masterfrom
MaxFreedomPollard:manual-backend-worker-poll-idle-work

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

ManualBackendWorker::poll() loops while (!_check_frontend_queues_and_cached_transit_events_empty()) poll_one(); and returns the moment the queues drain (include/quill/backend/ManualBackendWorker.h:161). poll(timeout) has the same shape. Each of those poll_one() calls therefore only ever takes the branch of BackendWorker::_poll() that processes a cached transit event.

Everything else the backend does lives in the branch a poll takes when it finds no cached transit events (include/quill/backend/BackendWorker.h:546-592): _flush_and_run_active_sinks(true, sink_min_flush_interval, SinkFlushReason::Periodic), _check_failure_counter(), _resync_rdtsc_clock() and, once the queues are also empty, _cleanup_invalidated_thread_contexts(), _cleanup_invalidated_loggers() and _try_shrink_empty_transit_event_buffers(). A polling loop built out of poll() can never reach it, because the loop exits exactly when a poll would have found nothing left to process.

So driving the backend the way the Basic Usage section of docs/manual_backend_worker.rst describes, "Periodically call poll() or poll(timeout)", as examples/manual_backend_worker.cpp does, never calls Sink::flush_sink() or Sink::run_periodic_tasks() while the application runs. A FileSink's records sit in the stdio buffer until it fills or shutdown() runs, so tail -f shows nothing and a crash loses the tail of the log. Dropped message notifications, the rdtsc resync BackendTscClock depends on, and the memory of removed loggers and retired thread contexts are skipped for the same reason. Calling poll_one() in your own loop is not affected: it hits the idle branch whenever there is nothing to process.

The fix polls once more after the loop exits because the queues drained. poll(timeout) returns instead when it runs out of budget with records still pending, so its incremental drain is unchanged and TEST_ManualBackendWorkerTimeoutPoll still sees a partial drain.

The regression test logs through a sink that counts flush_sink() and run_periodic_tasks() calls, then requires both counts to be non-zero after poll() and again after a poll(timeout) on already drained queues. It sets sink_min_flush_interval to 0 and log_timestamp_ordering_grace_period to 0 so the assertions do not depend on timing.

Verified on macOS arm64 with Apple clang 17, cmake -S . -B build -DQUILL_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug then cmake --build build --target <target> -j 4. On master the new test fails at the first flush assertion with REQUIRE_GT( 0, 0 ); with the change TEST_ManualBackendWorkerPollSinkFlush reports 1 test case and 4 assertions passing. TEST_ManualBackendWorker (18 assertions), TEST_ManualBackendWorkerTimeoutPoll (3), TEST_ManualBackendWorkerThreadMigration (14) and TEST_ManualBackendWorkerErrorNotifier (2) all still pass.

… the queues

ManualBackendWorker::poll() and poll(timeout) return as soon as
_check_frontend_queues_and_cached_transit_events_empty() reports the queues
drained, so they only ever run the branch of BackendWorker::_poll() that
processes a cached transit event.

Everything else the backend does lives in the branch that a poll takes when it
finds no cached transit events (include/quill/backend/BackendWorker.h:546-592):
flushing the sinks and running their periodic tasks, reporting dropped messages,
resyncing the rdtsc clock, and reclaiming removed loggers, retired thread
contexts and oversized transit event buffers. A polling loop built out of poll()
never reaches it, so Sink::flush_sink() and Sink::run_periodic_tasks() are never
called while the application runs. File sink records stay in the stdio buffer
until it fills or shutdown() runs.

poll() now polls once more after the loop exits because the queues drained.
poll(timeout) returns instead when it runs out of budget with records still
pending, so its incremental drain is unchanged.

Adds a regression test that logs through a sink counting flush_sink() and
run_periodic_tasks() calls, polls with poll() and poll(timeout), and requires
both counts to be non-zero.

@odygrd odygrd left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! looks good

@apozzer305
apozzer305 merged commit e3d1b97 into odygrd:master Sep 7, 2026
70 of 71 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants