Skip to content

perf: flush completion-tracked items on idle to bound Ack latency#20

Merged
acoshift merged 1 commit into
mainfrom
feat/flush-on-idle
Jun 13, 2026
Merged

perf: flush completion-tracked items on idle to bound Ack latency#20
acoshift merged 1 commit into
mainfrom
feat/flush-on-idle

Conversation

@acoshift

Copy link
Copy Markdown
Member

Follow-up to #19.

Problem

A lone IngestSync / IngestBatch item waited up to maxDelay (1s default) for the flush ticker before its first POST, so that interval was added synchronously to a pub/sub handler's Ack latency. The scrutinize pass on #19 flagged this as the one cost that genuinely matters at low volume.

Change

The worker now flushes as soon as the channel goes idle whenever the buffer holds a completion-tracked item (ack != nil). Those callers are blocked waiting to Ack, so they're latency-sensitive by nature. Before flushing it greedily drains whatever is already queued (up to batchSize), so:

  • a burst of concurrent IngestSync calls still coalesces into batches,
  • a bulk IngestBatch(N) still fills full batchSize batches (the items are already queued when the drain runs),
  • idle flush only fragments when items genuinely aren't available yet — i.e. exactly the low-volume case where the latency floor hurt and the extra requests are negligible.

Result: Ack latency is bounded by the round-trip instead of maxDelay.

Why no new API

It's gated on a tracked item being present (hasTracked), so pure fire-and-forget traffic never triggers it — its batching is byte-for-byte unchanged (only an extra ack != nil bool check per item on the hot path). No knob to set, self-tuning. The trade for tracked traffic is more, smaller requests at low/moderate volume in exchange for round-trip latency rather than maxDelay — which is the right trade for a caller blocking on durability.

Tests

3 new (ingest_sync_test.go): tracked item returns well under maxDelay; fire-and-forget still waits for the timer (guards the unchanged path); a tracked burst still delivers every record once, in order. Full suite green, go vet clean, race-clean across repeated/stress runs (timing-sensitive tests held over -count=5 -race).

🤖 Generated with Claude Code

A lone IngestSync/IngestBatch item previously waited up to maxDelay (1s
default) for the flush ticker before its first POST, so that interval was
added synchronously to a pub/sub handler's Ack latency.

The worker now flushes as soon as the channel goes idle whenever the buffer
holds a completion-tracked item (ack != nil) — those callers are blocked
waiting to Ack, so they are latency sensitive by nature. Before flushing it
greedily drains whatever is already queued (up to batchSize), so a burst still
coalesces into batches and a bulk IngestBatch still fills full batches; idle
flush only kicks in when items genuinely aren't available yet.

The behavior is gated on a tracked item being present, so pure fire-and-forget
traffic never triggers it and its batching is unchanged (only an extra
ack != nil check per item on the hot path). The trade for tracked traffic is
more, smaller requests at low/moderate volume in exchange for round-trip
latency instead of maxDelay.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@acoshift acoshift merged commit 86c4cf8 into main Jun 13, 2026
1 check passed
@acoshift acoshift deleted the feat/flush-on-idle branch June 13, 2026 06:01
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.

1 participant