fix(emitter): resolve idle-sleep and flush-timeout shutdown hangs - #100
Conversation
|
Thanks for your pull request. Is this your first contribution to a Snowplow open source project? Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://docs.snowplowanalytics.com/docs/contributing/contributor-license-agreement/ to learn more and sign. Once you've signed, please reply here (e.g. I signed it!) and we'll verify. Thanks. |
232a498 to
b458e63
Compare
Correctness gap:
|
…ea3b6d-snowplow-cpp-tracker)
b458e63 to
2dc3579
Compare
Re-review of latest push (
|
The C++ tracker's
Emitterhad three diagnosed shutdown-hang bugs, all fixed in this PR.Bug 1 — idle-sleep lost-wakeup (stop() blocks up to 5 s)
The background thread's idle sleep used a non-predicated
wait_for. Becausestop()callednotify_all()without holdingm_db_select, the notification could fire between the pre-check and thewait_for, causing the thread to sleep out the full 5 s interval before checking the stop flag.Fixed by introducing
std::atomic<bool> m_stop_requested{false}.stop()setsm_stop_requested = trueand then acquires/releasesm_db_selectbeforenotify_all(), closing the race: by the timestop()releases the lock the daemon is either already inwait_for(gets the notification) or will seem_stop_requested = trueat its next pre-check.start()resets the flag tofalseso the emitter can be restarted.Bug 2 — flush() hangs indefinitely on unreachable collector
flush()waited onm_check_finwith no timeout. If the collector was unreachable (bad DNS, network down),flush()— and thereforestop()— would hang forever.Fixed by replacing the infinite
waitwith await_forbounded by a configurable timeout:The timeout is exposed via
EmitterConfiguration::set_flush_timeout_ms()(default 30,000 ms; 0 = no timeout), following the existing setter pattern (set_batch_size,set_byte_limit_post, etc.). If the timeout expires before the queue drains,flush()callsstop()and returns; undelivered events remain in the SQLite event store for the next session.A dedicated
std::atomic<bool> m_flush_doneflag is set by the daemon when the queue empties (adjacent to the existingm_check_fin.notify_all()) and reset byflush()before waking the daemon, so the predicate only reflects state observed during thisflush()call.stop()also notifiesm_check_finafter joining the daemon thread so a concurrent externalstop()correctly unblocks a waitingflush().Bug 3 — retry sleep not interruptible
The per-batch retry delay used
sleep_for, whichstop()could not interrupt, causing stop to block for the full retry interval.Fixed by replacing
sleep_forwith a pre-check onm_stop_requestedplus a barewait_foronm_check_db, so a concurrentstop()wakes the retry sleep immediately.New tests
Four new tests added:
stop() on idle emitter returns promptly— asserts elapsed < 2,000 ms (measured: ~0.08 ms).flush() with unreachable collector returns after flush timeout— 500 ms config, 503-always client, asserts returns < 3,000 ms and events remain in SQLite.stop() during retry sleep returns promptly— asserts < 2,000 ms (measured: ~0.09 ms).EmitterConfiguration flush timeout getter/setter— round-trip assertion.Timing bounds are ≥ 2,000 ms to be reliable on loaded CI runners; actual latency is sub-millisecond.
What to review
emitter.cpp: pre-check + barewait_forinrun()(idle sleep and retry path);stop()lock-handshake closing the notification race;flush()timeout logic,m_flush_donepredicate, andstop()call after wait.emitter_configuration.hpp/.cpp:set_flush_timeout_ms/get_flush_timeout_mssetter/getter andshared_init()default.emitter.hpp:#include <atomic>,m_stop_requested{false},m_flush_done{false},m_flush_timeout_msdeclarations.Draft PR opened for review — please verify and run CI before merging.