@@ -40,10 +40,11 @@ use crate::xact_buffer::TxnSpanRegistry;
4040/// over the clone-into-owned baseline.
4141pub const DEFAULT_QUEUEING_BATCH_SIZE : usize = 64 ;
4242
43- /// Soft in-flight cap (channel batches + pump buffer). Past it the
44- /// pump yields so the worker drains. No hard cap: a permanently
45- /// stalled worker surfaces via the `wait_for_replay` timeout on the
46- /// shared err slot.
43+ /// Hard in-flight cap (channel batches + pump buffer). The channel is bounded
44+ /// at `max_records / batch_size`, so past it the pump's `send` blocks — real
45+ /// backpressure to the source instead of unbounded RAM growth. Deadlock-safe:
46+ /// shadow is fed by an independent walsender task (+keepalive), so a parked
47+ /// pump can't starve `wait_for_replay`.
4748pub const DEFAULT_QUEUEING_RECORD_SINK_CAPACITY : usize = 16_384 ;
4849
4950/// Worker `on_idle` cadence. Lets CH emitter's hold-INSERT-open
@@ -54,7 +55,7 @@ pub const DEFAULT_QUEUEING_IDLE_INTERVAL: Duration = Duration::from_millis(50);
5455/// Construct via [`QueueingRecordSink::spawn`].
5556pub struct QueueingRecordSink {
5657 /// Each batch carries its ship `Instant` for the worker's `queued_ms`.
57- tx : Option < mpsc:: UnboundedSender < ( Instant , Vec < Record < ' static > > ) > > ,
58+ tx : Option < mpsc:: Sender < ( Instant , Vec < Record < ' static > > ) > > ,
5859 /// Pump-side accumulator; shipped as one message at `batch_size` (or `close`).
5960 buf : Vec < Record < ' static > > ,
6061 batch_size : usize ,
@@ -63,7 +64,6 @@ pub struct QueueingRecordSink {
6364 /// Records the worker has dispatched; with `in_flight`, tells a draining
6465 /// queue from a stalled one.
6566 processed : Arc < AtomicU64 > ,
66- soft_cap : u64 ,
6767 worker : Option < JoinHandle < ( ) > > ,
6868 /// Per-txn span map; `Some` only with OTLP on. `flush_buf` stamps each
6969 /// shipped record's ship instant (`note_shipped`).
@@ -75,7 +75,7 @@ impl QueueingRecordSink {
7575 pub fn spawn < S > (
7676 inner : S ,
7777 batch_size : usize ,
78- soft_cap : usize ,
78+ max_records : usize ,
7979 span_registry : Option < TxnSpanRegistry > ,
8080 ) -> Self
8181 where
@@ -84,43 +84,46 @@ impl QueueingRecordSink {
8484 Self :: spawn_with_idle (
8585 inner,
8686 batch_size,
87- soft_cap ,
87+ max_records ,
8888 DEFAULT_QUEUEING_IDLE_INTERVAL ,
8989 span_registry,
9090 )
9191 }
9292
9393 /// Worker owns `inner`, drains batches, dispatches each record.
94- /// `soft_cap` triggers `yield_now` once in-flight (channel + pump
95- /// buffer) exceeds it. `idle_interval` paces `inner.on_idle()` on a
96- /// quiescent channel so time-based observer work (CH emitter's
97- /// hold-INSERT-open deadline) fires without fresh records.
94+ /// `max_records` bounds records in flight (channel + pump buffer):
95+ /// the channel holds `max_records / batch_size` batches, so a slow
96+ /// worker blocks the pump's send instead of growing without limit.
97+ /// `idle_interval` paces `inner.on_idle()` on a quiescent channel so
98+ /// time-based observer work (CH emitter's hold-INSERT-open deadline)
99+ /// fires without fresh records.
98100 pub fn spawn_with_idle < S > (
99101 mut inner : S ,
100102 batch_size : usize ,
101- soft_cap : usize ,
103+ max_records : usize ,
102104 idle_interval : Duration ,
103105 span_registry : Option < TxnSpanRegistry > ,
104106 ) -> Self
105107 where
106108 S : RecordSink + Send + ' static ,
107109 {
108- let ( tx, mut rx) = mpsc:: unbounded_channel :: < ( Instant , Vec < Record < ' static > > ) > ( ) ;
110+ let batch_size = batch_size. max ( 1 ) ;
111+ let channel_cap = ( max_records / batch_size) . max ( 1 ) ;
112+ let ( tx, mut rx) = mpsc:: channel :: < ( Instant , Vec < Record < ' static > > ) > ( channel_cap) ;
109113 let err = Arc :: new ( StdMutex :: new ( None ) ) ;
110114 let in_flight = Arc :: new ( AtomicU64 :: new ( 0 ) ) ;
111115 let processed = Arc :: new ( AtomicU64 :: new ( 0 ) ) ;
112116 let err_w = err. clone ( ) ;
113117 let in_flight_w = in_flight. clone ( ) ;
114118 let processed_w = processed. clone ( ) ;
115119 let reg_w = span_registry. clone ( ) ;
116- let batch_size = batch_size. max ( 1 ) ;
117120 let idle_interval = idle_interval. max ( Duration :: from_millis ( 1 ) ) ;
118121 let worker = tokio:: spawn ( async move {
119122 // Park error, drop in-flight (`n`, or 0 on idle path),
120123 // close+drain so `in_flight` settles. Caller breaks after.
121124 let park_err_and_drain = async |e : SinkError ,
122125 n : u64 ,
123- rx : & mut mpsc:: UnboundedReceiver < (
126+ rx : & mut mpsc:: Receiver < (
124127 Instant ,
125128 Vec < Record < ' static > > ,
126129 ) > | {
@@ -216,7 +219,6 @@ impl QueueingRecordSink {
216219 err,
217220 in_flight,
218221 processed,
219- soft_cap : soft_cap. max ( 1 ) as u64 ,
220222 worker : Some ( worker) ,
221223 span_registry,
222224 }
@@ -239,10 +241,10 @@ impl QueueingRecordSink {
239241 if let Some ( e) = self . take_pending_error ( ) {
240242 return Err ( e) ;
241243 }
242- self . flush_buf ( )
244+ self . flush_buf ( ) . await
243245 }
244246
245- fn flush_buf ( & mut self ) -> Result < ( ) , SinkError > {
247+ async fn flush_buf ( & mut self ) -> Result < ( ) , SinkError > {
246248 if self . buf . is_empty ( ) {
247249 return Ok ( ( ) ) ;
248250 }
@@ -260,7 +262,9 @@ impl QueueingRecordSink {
260262 . tx
261263 . as_ref ( )
262264 . ok_or_else ( || SinkError :: Other ( "queueing record sink already closed" . into ( ) ) ) ?;
263- if tx. send ( ( Instant :: now ( ) , batch) ) . is_err ( ) {
265+ // Blocking the pump here is deadlock-safe: shadow is fed by an independent
266+ // walsender task (+keepalive), so a parked pump can't starve wait_for_replay.
267+ if tx. send ( ( Instant :: now ( ) , batch) ) . await . is_err ( ) {
264268 self . in_flight . fetch_sub ( n, Ordering :: Relaxed ) ;
265269 if let Some ( e) = self . take_pending_error ( ) {
266270 return Err ( e) ;
@@ -276,7 +280,7 @@ impl QueueingRecordSink {
276280 /// Call after the pump stops feeding records.
277281 pub async fn close ( mut self ) -> Result < ( ) , SinkError > {
278282 // Flush tail before dropping sender so worker sees final batch.
279- self . flush_buf ( ) ?;
283+ self . flush_buf ( ) . await ?;
280284 self . tx . take ( ) ;
281285 if let Some ( handle) = self . worker . take ( ) {
282286 // Treat a worker panic as a sink error so daemon shutdown
@@ -336,12 +340,7 @@ impl RecordSink for QueueingRecordSink {
336340 catalog_signal : record. catalog_signal ,
337341 } ) ;
338342 if self . buf . len ( ) >= self . batch_size {
339- self . flush_buf ( ) ?;
340- // Soft backpressure: yield only when actually behind,
341- // checked at flush time to keep per-record cost low.
342- if self . in_flight . load ( Ordering :: Relaxed ) > self . soft_cap {
343- tokio:: task:: yield_now ( ) . await ;
344- }
343+ self . flush_buf ( ) . await ?;
345344 }
346345 Ok ( ( ) )
347346 } )
0 commit comments