Skip to content

Count flush trigger reseeded from the ETA backlog caps batch size on countdown queues #109

Description

@dmcwhorter

Summary

On a task whose messages carry an ETA/countdown, the count-based flush trigger is reseeded from the number of parked requests:

if len(ready_requests) > 0:
    logger.debug("Batches: Ready buffer complete: %s", len(ready_requests))
    self.flush(ready_requests)
    self._count = count(self._pending.qsize() + 1)   # celery_batches/__init__.py#L345

_pending holds requests whose ETA has not elapsed. On a delayed queue that backlog is large and roughly constant, so the next count flush fires after flush_every - len(_pending) further arrivals instead of flush_every. The trigger no longer tracks how full _buffer is, and mean batch size plateaus near half of flush_every at any arrival rate.

Queues with no ETA are unaffected: their _pending is always empty, so count(_pending.qsize() + 1) and count(1) are the same.

Reproduction

Discrete-event simulation driving the real Strategy() handler and _do_flush against a virtual clock. Poisson arrivals, flush_every=16, flush_interval=5, a 120 s countdown on every message, 5 seeds × 1800 s. Only monotonic, to_timestamp and the request factory are faked; the trigger logic is unmodified.

arrival (msg/s) mean batch, current mean batch, count(1)
1 4.0 5.0
3 6.3 11.2
5 6.7 12.2
13.9 7.6 14.4
40 8.0 15.4

Current behaviour plateaus at 8.0/16 however high arrival goes; reseeding from 1 reaches 15.4/16.

The invariant break is visible directly: at 13.9 msg/s, 212 of 212 count-triggered flushes carried 12 requests against flush_every=16. A count-triggered flush should mean flush_every messages were buffered.

Why the current seed backfires

Seeding with _pending.qsize() + 1 makes the next flush fire sooner when a backlog exists, which reads as "there is parked work, so check more often". But parked work is not gated on checking — only on its ETA elapsing. The extra wake-ups cannot release anything early; they only chop _buffer into smaller batches, which is the opposite of what the count trigger is for.

Suggested fix

-            self._count = count(self._pending.qsize() + 1)
+            self._count = count(1)

The counter counts arrivals into _buffer. _pending is a separate population the count trigger does not gate, so folding it into the seed conflates the two.

Happy to open a PR with a regression test if you'd like the change in this shape — and equally happy to be told the current seed is deliberate for a case I've missed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions