1010package stream
1111
1212import (
13+ "container/list"
1314 "context"
15+ "fmt"
16+ "math"
1417 "sync"
1518)
1619
@@ -25,6 +28,7 @@ const defaultMaxInflight = 1_000_000
2528type item [Req any ] struct {
2629 offset int64
2730 payload Req
31+ weight int64
2832}
2933
3034type discardResult struct {
@@ -33,6 +37,14 @@ type discardResult struct {
3337 count int
3438}
3539
40+ type capacityWaiter struct {
41+ weight int64
42+ ready chan struct {}
43+ granted bool
44+ err error
45+ elem * list.Element
46+ }
47+
3648// buffer is the bounded, offset-assigning queue between the caller's Ingest
3749// calls and the sender goroutine. Callers enqueue already-encoded messages;
3850// the sender observes them (moves to in-flight); acks cause them to be
@@ -46,72 +58,145 @@ type discardResult struct {
4658// - The supervisor calls requeue and drain, but only while the sender is
4759// stopped — so next never runs concurrently with requeue or drain.
4860//
49- // All state (queue, flight, sem , cond) is private; the sender and receiver
61+ // All state (queue, flight, capacity , cond) is private; the sender and receiver
5062// interact only through these methods, never by touching the fields directly.
5163//
52- // The semaphore enforces the MaxInflight cap: enqueue blocks once the cap is
53- // reached and unblocks as acks arrive and discardThrough releases permits.
64+ // Count and payload-byte limits apply to queued plus in-flight items.
5465type buffer [Req any ] struct {
55- mu sync.Mutex
56- cond * sync.Cond
57- queue []item [Req ] // pending: enqueued but not yet observed by the sender
58- flight []item [Req ] // in-flight: observed by the sender, waiting for ack
59- closed bool
60- sem chan struct {} // capacity = maxInflight; held while item is in queue or flight
61- doneOnce sync.Once
62- doneCh chan struct {} // closed when the buffer is closed/drained; unblocks sem waiters
66+ mu sync.Mutex
67+ cond * sync.Cond
68+ queue []item [Req ] // pending: enqueued but not yet observed by the sender
69+ flight []item [Req ] // in-flight: observed by the sender, waiting for ack
70+ closed bool
71+ maxInflight int
72+ maxBufferedBytes int64
73+ usedItems int
74+ usedBytes int64
75+ accountingReset bool
76+ waiters * list.List
6377}
6478
65- func newBuffer [Req any ](maxInflight int ) * buffer [Req ] {
66- // Normalize a non-positive cap, which would otherwise deadlock (0) or
67- // panic (<0) at the semaphore .
79+ func newBuffer [Req any ](maxInflight int , byteLimit int64 ) * buffer [Req ] {
80+ // Normalize a non-positive count cap, which would otherwise prevent every
81+ // reservation from being granted .
6882 if maxInflight <= 0 {
6983 maxInflight = defaultMaxInflight
7084 }
71- // queue/flight grow on demand; don't preallocate to maxInflight, which with
72- // the default 1M cap would reserve tens of MB per stream before a single
73- // record is ingested. Only the semaphore is sized to the cap, since it is the
74- // backpressure gate and its capacity defines the bound.
85+ maxBufferedBytes := int64 ( math . MaxInt64 )
86+ if byteLimit > 0 {
87+ maxBufferedBytes = byteLimit
88+ }
7589 b := & buffer [Req ]{
76- sem : make (chan struct {}, maxInflight ),
77- doneCh : make (chan struct {}),
90+ maxInflight : maxInflight ,
91+ maxBufferedBytes : maxBufferedBytes ,
92+ waiters : list .New (),
7893 }
7994 b .cond = sync .NewCond (& b .mu )
8095 return b
8196}
8297
83- func (b * buffer [Req ]) closeDone () {
84- b .doneOnce .Do (func () { close (b .doneCh ) })
85- }
86-
87- // reserve acquires one backpressure slot.
88- func (b * buffer [Req ]) reserve (ctx context.Context ) error {
98+ // reserve acquires one item slot and payload-byte weight.
99+ func (b * buffer [Req ]) reserve (ctx context.Context , weight int64 ) error {
89100 if err := ctx .Err (); err != nil {
90101 return err
91102 }
92- select {
93- case b .sem <- struct {}{}:
103+ if weight < 0 || weight > b .maxBufferedBytes {
104+ return fmt .Errorf ("%w: buffered payload weight %d exceeds limit %d" ,
105+ ErrPayloadTooLarge , weight , b .maxBufferedBytes )
106+ }
107+ b .mu .Lock ()
108+ if b .closed {
109+ b .mu .Unlock ()
110+ return errClosed
111+ }
112+ if b .waiters .Len () == 0 && b .canReserveLocked (weight ) {
113+ b .usedItems ++
114+ b .usedBytes += weight
115+ b .mu .Unlock ()
94116 return nil
117+ }
118+ waiter := & capacityWaiter {weight : weight , ready : make (chan struct {})}
119+ waiter .elem = b .waiters .PushBack (waiter )
120+ b .mu .Unlock ()
121+
122+ select {
123+ case <- waiter .ready :
124+ return waiter .err
95125 case <- ctx .Done ():
126+ b .mu .Lock ()
127+ if waiter .granted {
128+ // drain may have closed the buffer and reset accounting after this
129+ // reservation was granted but before cancellation won the select.
130+ if ! b .accountingReset {
131+ b .usedItems --
132+ b .usedBytes -= weight
133+ }
134+ } else if waiter .err == nil {
135+ if waiter .elem != nil {
136+ b .waiters .Remove (waiter .elem )
137+ waiter .elem = nil
138+ }
139+ }
140+ b .grantWaitersLocked ()
141+ b .mu .Unlock ()
96142 return ctx .Err ()
97- case <- b .doneCh :
98- return errClosed
99143 }
100144}
101145
102- func (b * buffer [Req ]) release () {
103- <- b .sem
146+ func (b * buffer [Req ]) canReserveLocked (weight int64 ) bool {
147+ return b .usedItems < b .maxInflight &&
148+ weight <= b .maxBufferedBytes - b .usedBytes
149+ }
150+
151+ func (b * buffer [Req ]) grantWaitersLocked () {
152+ for ! b .closed && b .waiters .Len () > 0 {
153+ front := b .waiters .Front ()
154+ waiter := front .Value .(* capacityWaiter )
155+ if ! b .canReserveLocked (waiter .weight ) {
156+ return
157+ }
158+ b .waiters .Remove (front )
159+ waiter .elem = nil
160+ b .usedItems ++
161+ b .usedBytes += waiter .weight
162+ waiter .granted = true
163+ close (waiter .ready )
164+ }
165+ }
166+
167+ func (b * buffer [Req ]) failWaitersLocked (err error ) {
168+ for element := b .waiters .Front (); element != nil ; element = element .Next () {
169+ waiter := element .Value .(* capacityWaiter )
170+ waiter .err = err
171+ waiter .elem = nil
172+ close (waiter .ready )
173+ }
174+ b .waiters .Init ()
175+ }
176+
177+ func (b * buffer [Req ]) release (weight int64 ) {
178+ b .mu .Lock ()
179+ if ! b .accountingReset {
180+ b .usedItems --
181+ b .usedBytes -= weight
182+ b .grantWaitersLocked ()
183+ }
184+ b .mu .Unlock ()
104185}
105186
106187// append adds an item after reserve succeeds.
107- func (b * buffer [Req ]) append (offset int64 , msg Req ) error {
188+ func (b * buffer [Req ]) append (offset int64 , msg Req , weight int64 ) error {
108189 b .mu .Lock ()
109190 if b .closed {
191+ if ! b .accountingReset {
192+ b .usedItems --
193+ b .usedBytes -= weight
194+ }
110195 b .mu .Unlock ()
111- b .release ()
196+ b .cond . Broadcast ()
112197 return errClosed
113198 }
114- b .queue = append (b .queue , item [Req ]{offset : offset , payload : msg })
199+ b .queue = append (b .queue , item [Req ]{offset : offset , payload : msg , weight : weight })
115200 b .mu .Unlock ()
116201 b .cond .Signal ()
117202 return nil
@@ -163,30 +248,28 @@ func (b *buffer[Req]) next(ctx context.Context) (item[Req], error) {
163248}
164249
165250// discardThrough removes every in-flight item whose offset is <= offset (all
166- // now acknowledged by the server) and releases one semaphore slot per removed
167- // item so blocked enqueue callers can proceed. It is the sender/receiver's only
168- // hook for ack-driven eviction, keeping the buffer's internals (flight, sem,
169- // cond) private. Returns the contiguous discarded offset range without
170- // allocating per-item callback metadata.
251+ // now acknowledged by the server), releases its count and byte capacity, and
252+ // grants queued admission waiters in FIFO order. It is the receiver's only hook
253+ // for ack-driven eviction. Returns the contiguous discarded offset range
254+ // without allocating per-item callback metadata.
171255func (b * buffer [Req ]) discardThrough (offset int64 ) discardResult {
172256 b .mu .Lock ()
173257 var result discardResult
258+ var releasedBytes int64
174259 for len (b .flight ) > 0 && b .flight [0 ].offset <= offset {
175260 if result .count == 0 {
176261 result .first = b .flight [0 ].offset
177262 }
178263 result .last = b .flight [0 ].offset
179264 result .count ++
265+ releasedBytes += b .flight [0 ].weight
180266 b .flight [0 ] = item [Req ]{} // release the acked payload for GC
181267 b .flight = b .flight [1 :]
182268 }
269+ b .usedItems -= result .count
270+ b .usedBytes -= releasedBytes
271+ b .grantWaitersLocked ()
183272 b .mu .Unlock ()
184- for range result .count {
185- <- b .sem
186- }
187- if result .count > 0 {
188- b .cond .Broadcast ()
189- }
190273 return result
191274}
192275
@@ -239,9 +322,12 @@ func (b *buffer[Req]) drain() []item[Req] {
239322 b .queue = nil
240323 b .flight = nil
241324 b .closed = true
325+ b .accountingReset = true
326+ b .usedItems = 0
327+ b .usedBytes = 0
328+ b .failWaitersLocked (errClosed )
242329 b .mu .Unlock ()
243330 b .cond .Broadcast ()
244- b .closeDone ()
245331 return all
246332}
247333
@@ -250,9 +336,9 @@ func (b *buffer[Req]) drain() []item[Req] {
250336func (b * buffer [Req ]) close () {
251337 b .mu .Lock ()
252338 b .closed = true
339+ b .failWaitersLocked (errClosed )
253340 b .mu .Unlock ()
254341 b .cond .Broadcast ()
255- b .closeDone ()
256342}
257343
258344// inFlight returns the number of items observed by the sender but not yet
0 commit comments