@@ -27,7 +27,7 @@ use crate::error::LiteError;
2727use crate :: runtime:: now_millis_i64;
2828use crate :: storage:: engine:: { StorageEngine , WriteOp } ;
2929#[ cfg( not( target_arch = "wasm32" ) ) ]
30- use crate :: sync:: ColumnarOutbound ;
30+ use crate :: sync:: outbound :: columnar :: ColumnarOutbound ;
3131#[ cfg( not( target_arch = "wasm32" ) ) ]
3232use crate :: sync:: outbound:: timeseries:: TimeseriesOutbound ;
3333
@@ -138,7 +138,7 @@ pub struct ColumnarEngine<S: StorageEngine> {
138138 /// Optional outbound queue for plain columnar insert sync.
139139 /// `None` when sync is disabled or not yet configured.
140140 #[ cfg( not( target_arch = "wasm32" ) ) ]
141- outbound : Option < Arc < ColumnarOutbound > > ,
141+ outbound : Option < Arc < ColumnarOutbound < S > > > ,
142142 /// Optional outbound queue for timeseries-profile insert sync.
143143 ///
144144 /// Timeseries collections must use `TimeseriesPush` frames on Origin
@@ -147,7 +147,7 @@ pub struct ColumnarEngine<S: StorageEngine> {
147147 /// collections with `ColumnarProfile::Timeseries` are enqueued here
148148 /// instead of `outbound`.
149149 #[ cfg( not( target_arch = "wasm32" ) ) ]
150- timeseries_outbound : Option < Arc < TimeseriesOutbound > > ,
150+ timeseries_outbound : Option < Arc < TimeseriesOutbound < S > > > ,
151151}
152152
153153impl < S : StorageEngine > ColumnarEngine < S > {
@@ -167,7 +167,7 @@ impl<S: StorageEngine> ColumnarEngine<S> {
167167 ///
168168 /// Must be called before any inserts if columnar sync is desired.
169169 #[ cfg( not( target_arch = "wasm32" ) ) ]
170- pub fn set_outbound ( & mut self , outbound : Arc < ColumnarOutbound > ) {
170+ pub fn set_outbound ( & mut self , outbound : Arc < ColumnarOutbound < S > > ) {
171171 self . outbound = Some ( outbound) ;
172172 }
173173
@@ -177,7 +177,7 @@ impl<S: StorageEngine> ColumnarEngine<S> {
177177 /// are routed here instead of `outbound`, so the transport can send them
178178 /// as `TimeseriesPush` frames to Origin's timeseries engine.
179179 #[ cfg( not( target_arch = "wasm32" ) ) ]
180- pub fn set_timeseries_outbound ( & mut self , outbound : Arc < TimeseriesOutbound > ) {
180+ pub fn set_timeseries_outbound ( & mut self , outbound : Arc < TimeseriesOutbound < S > > ) {
181181 self . timeseries_outbound = Some ( outbound) ;
182182 }
183183
@@ -518,32 +518,103 @@ impl<S: StorageEngine> ColumnarEngine<S> {
518518
519519 /// Insert a row into a columnar collection's memtable.
520520 ///
521- /// When a sync outbound queue is attached the row is also enqueued for
522- /// replication to Origin. Timeseries-profile collections use the
523- /// `timeseries_outbound` queue (→ `TimeseriesPush` frames); all other
524- /// columnar collections use `outbound` (→ `ColumnarInsert` frames).
521+ /// This is a pure in-memory operation. Call [`enqueue_outbound`] from
522+ /// an async context after inserting to durably enqueue the row for
523+ /// replication to Origin.
525524 pub fn insert ( & self , collection : & str , values : & [ Value ] ) -> Result < ( ) , LiteError > {
526525 let state_arc = self . lookup ( collection) ?;
527526 let mut s = Self :: lock_state ( & state_arc) ?;
528527 s. mutation . insert ( values) . map_err ( columnar_err_to_lite) ?;
528+ Ok ( ( ) )
529+ }
529530
530- #[ cfg( not( target_arch = "wasm32" ) ) ]
531- if matches ! ( s. profile, ColumnarProfile :: Timeseries { .. } ) {
532- // Timeseries rows must replicate via TimeseriesPush so that Origin
533- // stores them in its timeseries engine, not the columnar MutationEngine.
534- if let Some ( ts_out) = & self . timeseries_outbound {
535- let column_names: Vec < String > = s
536- . mutation
537- . schema ( )
538- . columns
539- . iter ( )
540- . map ( |c| c. name . clone ( ) )
541- . collect ( ) ;
542- ts_out. enqueue_row ( collection, column_names, values. to_vec ( ) ) ;
531+ /// Durably enqueue a batch of inserted rows for replication to Origin.
532+ ///
533+ /// Must be called from an async context after one or more successful
534+ /// [`insert`] calls. Timeseries-profile collections are routed to the
535+ /// `timeseries_outbound` queue; all other columnar collections use the
536+ /// plain `outbound` queue.
537+ ///
538+ /// Returns [`LiteError::Backpressure`] when the queue is at cap so the
539+ /// caller can propagate back-pressure. Other enqueue errors are logged as
540+ /// warnings (the local insert already succeeded) and `Ok(())` is returned.
541+ #[ cfg( not( target_arch = "wasm32" ) ) ]
542+ pub async fn enqueue_outbound (
543+ & self ,
544+ collection : & str ,
545+ rows : & [ Vec < Value > ] ,
546+ ) -> Result < ( ) , LiteError > {
547+ if rows. is_empty ( ) {
548+ return Ok ( ( ) ) ;
549+ }
550+
551+ // Read the profile and schema metadata under the lock, then drop it
552+ // before any await point to satisfy the no-lock-across-await rule.
553+ enum OutboundRoute {
554+ Timeseries { column_names : Vec < String > } ,
555+ Columnar { schema_bytes : Vec < u8 > } ,
556+ None ,
557+ }
558+
559+ let route: OutboundRoute = {
560+ let state_arc = self . lookup ( collection) ?;
561+ let s = Self :: lock_state ( & state_arc) ?;
562+ if matches ! ( s. profile, ColumnarProfile :: Timeseries { .. } ) {
563+ if self . timeseries_outbound . is_some ( ) {
564+ let column_names: Vec < String > = s
565+ . mutation
566+ . schema ( )
567+ . columns
568+ . iter ( )
569+ . map ( |c| c. name . clone ( ) )
570+ . collect ( ) ;
571+ OutboundRoute :: Timeseries { column_names }
572+ } else {
573+ OutboundRoute :: None
574+ }
575+ } else if self . outbound . is_some ( ) {
576+ let schema_bytes = zerompk:: to_msgpack_vec ( s. mutation . schema ( ) ) . unwrap_or_default ( ) ;
577+ OutboundRoute :: Columnar { schema_bytes }
578+ } else {
579+ OutboundRoute :: None
580+ }
581+ // lock `s` is dropped here at end of block
582+ } ;
583+
584+ match route {
585+ OutboundRoute :: Timeseries { column_names } => {
586+ let queue = match & self . timeseries_outbound {
587+ Some ( q) => Arc :: clone ( q) ,
588+ None => return Ok ( ( ) ) ,
589+ } ;
590+ for row in rows {
591+ crate :: sync:: reconcile_outbound_enqueue (
592+ queue
593+ . enqueue_row ( collection, column_names. clone ( ) , row. clone ( ) )
594+ . await ,
595+ "timeseries insert" ,
596+ collection,
597+ "" ,
598+ ) ?;
599+ }
600+ }
601+ OutboundRoute :: Columnar { schema_bytes } => {
602+ let queue = match & self . outbound {
603+ Some ( q) => Arc :: clone ( q) ,
604+ None => return Ok ( ( ) ) ,
605+ } ;
606+ for row in rows {
607+ crate :: sync:: reconcile_outbound_enqueue (
608+ queue
609+ . enqueue_row ( collection, row. clone ( ) , schema_bytes. clone ( ) )
610+ . await ,
611+ "columnar insert" ,
612+ collection,
613+ "" ,
614+ ) ?;
615+ }
543616 }
544- } else if let Some ( outbound) = & self . outbound {
545- let schema_bytes = zerompk:: to_msgpack_vec ( s. mutation . schema ( ) ) . unwrap_or_default ( ) ;
546- outbound. enqueue_row ( collection, values. to_vec ( ) , schema_bytes) ;
617+ OutboundRoute :: None => { }
547618 }
548619
549620 Ok ( ( ) )
0 commit comments