@@ -27,6 +27,8 @@ use storage::storage::Storage;
2727use tokio:: sync:: { Semaphore , mpsc, oneshot} ;
2828use tokio:: time:: timeout;
2929
30+ const COMMAND_TIMEOUT : Duration = Duration :: from_secs ( 30 ) ;
31+
3032/// Configuration for pipeline processing
3133#[ derive( Debug , Clone ) ]
3234pub struct PipelineConfig {
@@ -113,15 +115,16 @@ pub struct CommandPipeline {
113115
114116impl CommandPipeline {
115117 pub fn new (
116- config : PipelineConfig ,
118+ mut config : PipelineConfig ,
117119 storage : Arc < Storage > ,
118120 cmd_table : Arc < CmdTable > ,
119121 executor : Arc < CmdExecutor > ,
120122 ) -> Self {
121123 // Bounded channel enforces backpressure: when the queue is full,
122124 // senders await instead of growing the queue unboundedly.
123125 // Guard against 0 (rendezvous) to avoid unintentional stalls.
124- let ( command_tx, command_rx) = mpsc:: channel ( config. command_queue_size . max ( 1 ) ) ;
126+ config. command_queue_size = config. command_queue_size . max ( 1 ) ;
127+ let ( command_tx, command_rx) = mpsc:: channel ( config. command_queue_size ) ;
125128 let semaphore = Arc :: new ( Semaphore :: new ( config. max_concurrent_pipelines ) ) ;
126129
127130 let pipeline = Self {
@@ -155,18 +158,7 @@ impl CommandPipeline {
155158 response_tx,
156159 } ;
157160
158- // Send command to pipeline. On a full bounded channel this awaits
159- // (backpressure) rather than growing the queue indefinitely.
160- self . command_tx
161- . send ( command)
162- . await
163- . map_err ( |_| PipelineError :: ChannelClosed ) ?;
164-
165- // Wait for response with timeout
166- timeout ( Duration :: from_secs ( 30 ) , response_rx)
167- . await
168- . map_err ( |_| PipelineError :: Timeout ) ?
169- . map_err ( |_| PipelineError :: ResponseChannelClosed )
161+ send_and_receive ( & self . command_tx , command, response_rx, COMMAND_TIMEOUT ) . await
170162 }
171163
172164 /// Start the background batch processor
@@ -396,12 +388,122 @@ pub enum PipelineError {
396388 Timeout ,
397389}
398390
391+ async fn send_and_receive (
392+ command_tx : & mpsc:: Sender < PipelineCommand > ,
393+ command : PipelineCommand ,
394+ response_rx : oneshot:: Receiver < RespData > ,
395+ request_timeout : Duration ,
396+ ) -> Result < RespData , PipelineError > {
397+ timeout ( request_timeout, async {
398+ command_tx
399+ . send ( command)
400+ . await
401+ . map_err ( |_| PipelineError :: ChannelClosed ) ?;
402+ response_rx
403+ . await
404+ . map_err ( |_| PipelineError :: ResponseChannelClosed )
405+ } )
406+ . await
407+ . map_err ( |_| PipelineError :: Timeout ) ?
408+ }
409+
399410#[ allow( clippy:: unwrap_used) ]
400411#[ cfg( test) ]
401412mod tests {
402413 use super :: * ;
403414 use std:: time:: Duration ;
404- use tokio:: time:: sleep;
415+ use tokio:: time:: { advance, sleep} ;
416+
417+ struct TestStream ;
418+
419+ #[ async_trait:: async_trait]
420+ impl client:: StreamTrait for TestStream {
421+ async fn read ( & mut self , _buf : & mut [ u8 ] ) -> Result < usize , std:: io:: Error > {
422+ Ok ( 0 )
423+ }
424+
425+ async fn write ( & mut self , _data : & [ u8 ] ) -> Result < usize , std:: io:: Error > {
426+ Ok ( 0 )
427+ }
428+ }
429+
430+ fn pipeline_command ( response_tx : oneshot:: Sender < RespData > ) -> PipelineCommand {
431+ PipelineCommand {
432+ data : RespData :: Array ( Some ( Vec :: new ( ) ) ) ,
433+ client : Arc :: new ( Client :: new ( Box :: new ( TestStream ) ) ) ,
434+ received_at : Instant :: now ( ) ,
435+ response_tx,
436+ }
437+ }
438+
439+ #[ tokio:: test( start_paused = true ) ]
440+ async fn queue_wait_and_response_share_single_deadline ( ) {
441+ let ( command_tx, mut command_rx) = mpsc:: channel ( 1 ) ;
442+ let ( filler_response_tx, _filler_response_rx) = oneshot:: channel ( ) ;
443+ command_tx
444+ . send ( pipeline_command ( filler_response_tx) )
445+ . await
446+ . expect ( "filler command must fit in the queue" ) ;
447+
448+ let ( target_response_tx, target_response_rx) = oneshot:: channel ( ) ;
449+ let target_command = pipeline_command ( target_response_tx) ;
450+ let submit_task = tokio:: spawn ( async move {
451+ send_and_receive (
452+ & command_tx,
453+ target_command,
454+ target_response_rx,
455+ Duration :: from_secs ( 30 ) ,
456+ )
457+ . await
458+ } ) ;
459+ tokio:: task:: yield_now ( ) . await ;
460+
461+ advance ( Duration :: from_secs ( 20 ) ) . await ;
462+ let filler_command = command_rx
463+ . recv ( )
464+ . await
465+ . expect ( "filler command must remain queued" ) ;
466+ drop ( filler_command) ;
467+ tokio:: task:: yield_now ( ) . await ;
468+
469+ advance ( Duration :: from_secs ( 11 ) ) . await ;
470+ tokio:: task:: yield_now ( ) . await ;
471+ assert ! (
472+ submit_task. is_finished( ) ,
473+ "queue wait and response wait must share one deadline"
474+ ) ;
475+ let result = submit_task. await . expect ( "submission task must not panic" ) ;
476+ assert ! ( matches!( result, Err ( PipelineError :: Timeout ) ) ) ;
477+
478+ let target_command = command_rx
479+ . recv ( )
480+ . await
481+ . expect ( "target command must have entered the queue" ) ;
482+ assert ! (
483+ target_command
484+ . response_tx
485+ . send( RespData :: SimpleString ( "late" . into( ) ) )
486+ . is_err( ) ,
487+ "timing out must close the target response receiver"
488+ ) ;
489+ }
490+
491+ #[ tokio:: test]
492+ async fn zero_command_queue_size_reports_effective_capacity ( ) {
493+ let config = PipelineConfig {
494+ command_queue_size : 0 ,
495+ ..Default :: default ( )
496+ } ;
497+ let pipeline = CommandPipeline :: new (
498+ config,
499+ Arc :: new ( Storage :: new ( 1 , 0 ) ) ,
500+ Arc :: new ( CmdTable :: new ( ) ) ,
501+ Arc :: new ( CmdExecutor :: new ( 1 , 1 ) ) ,
502+ ) ;
503+
504+ assert_eq ! ( pipeline. command_tx. max_capacity( ) , 1 ) ;
505+ assert_eq ! ( pipeline. stats( ) . queue_capacity, 1 ) ;
506+ }
405507
406508 #[ tokio:: test]
407509 async fn test_pipeline_basic ( ) {
0 commit comments