/claim #9810

Problem

ZStream.buffer(n) was buffering n + 1 elements instead of n. The upstream producer eagerly computes one element before offering it to the internal bounded queue, so with a queue of capacity n, the total in-flight count was always n + 1.

Reproducer (from the issue): buffer(1) allows 3 elements to be produced while the consumer is still processing the first one.

Root cause

As described by @kyri-petrou in the issue discussion, the sequence is:

  1. fakeNetworkCall(1) — computed eagerly
  2. offer — accepted, polled immediately downstream
  3. fakeNetworkCall(2) — computed eagerly
  4. offer — added to queue (capacity 1), queue full
  5. fakeNetworkCall(3) — computed eagerly ← this is the extra element
  6. offer — blocks because queue is full

Fix

Three cases, following the approach approved by @jdegoes:

Capacity Strategy Why
<= 0 No-op (return stream unchanged) No buffering requested
== 1 Synchronous Handoff Can’t use Queue.bounded(0). Handoff blocks the producer until the consumer takes, so exactly 1 element is buffered ahead.
>= 2 Queue.bounded(capacity - 1) Queue capacity (n-1) + 1 eagerly computed element = exactly n total

The processLoop helper is extracted to avoid duplicating the channel drain logic across the handoff and queue paths.

Tests added

Test What it verifies
buffer(1) does not prefetch a third element After pulling element 1, the producer offers element 2 into the handoff and blocks — element 3 does not start. Repeated 100x (@@ nonFlaky).
buffer(2) allows a third element to start Confirms that buffer(2) does allow 3 elements in flight (correct behaviour for capacity 2).
buffer(1) maintains elements and ordering Property-based test ensuring correctness is preserved with the new handoff path.

Validation

sbt "streamsTestsJVM/testOnly zio.stream.ZStreamSpec -- -t buffer"
23 tests passed. 0 tests failed. 0 tests ignored.

All existing buffer tests continue to pass.

Update - Video recording of the fix: https://drive.google.com/file/d/1w3NtJsVE2xbn05m9LP0evoVKairaMDKA/view?usp=sharing Shows all 23 buffer tests passing, including 3 new regression tests:

  • buffer(1) does not prefetch a third element (repeated 100x)
  • buffer(2) allows a third element to start
  • buffer(1) maintains elements and ordering

Claim

Total prize pool $250
Total paid $0
Status Pending
Submitted March 17, 2026
Last updated March 17, 2026

Contributors

HU

Huntersreeni

@HunterSreeni

100%

Sponsors

ZI

ZIO

@ZIO

$250