Skip to content

Lost-wakeup race in the Logs/Metrics batch processor can strand queued events past the flush interval #5739

Description

@tsushanth

Description

The add -> maybeSchedule fast path reads the hasScheduled flag outside scheduleLock, while the running flush resets that flag inside scheduleLock after a separate queue.isEmpty() check. This is a check-then-act TOCTOU: an event offered by a producer thread in the window after the flush reads isEmpty() == true but before it writes hasScheduled = false is neither observed by the flush (not rescheduled) nor able to schedule itself (the fast path sees hasScheduled still true). The event is left queued with hasScheduled == false and no pending flush. Affects both LoggerBatchProcessor and MetricsBatchProcessor (byte-identical scheduling logic).

Root cause

  • sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java: maybeSchedule reads hasScheduled outside the lock; flush() re-checks isEmpty() then sets hasScheduled = false inside the lock.
  • sentry/src/main/java/io/sentry/metrics/MetricsBatchProcessor.java: same structure.

The stranded event is only sent on a subsequent add() or an explicit flush()/close(). On Android this is partly masked because AndroidLoggerBatchProcessor/AndroidMetricsBatchProcessor force a flush on onBackground().

Note: reproduced as a deterministic interleaving of the state machine (same volatile flag, ReentrantLock, ConcurrentLinkedQueue), not a live Android run.

Suggested fix

Make the flag reset and queue re-check safe against the lock-free producer path: after draining, set hasScheduled = false first and then re-check queue.isEmpty() under the lock (rescheduling if non-empty), or remove the lock-free fast path so maybeSchedule always consults the queue under scheduleLock.

Metadata

Metadata

Assignees

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions