@@ -454,6 +454,7 @@ impl FjallStorage {
454454 pub fn receive_loop ( & mut self , receiver : EffectReceiver ) {
455455 let mut slow_queue = SlowQueueAggregator :: default ( ) ;
456456 let mut group: Vec < EffectHandle > = Vec :: new ( ) ;
457+ let mut group_index: Option < PendingWriteIndex > = None ;
457458 loop {
458459 let Ok ( first) = receiver. recv ( ) else {
459460 tracing:: warn!( "Storage receiver channel closed, shutting down storage thread." ) ;
@@ -470,17 +471,30 @@ impl FjallStorage {
470471
471472 for item in pending {
472473 if is_groupable_write ( & item. 0 ) {
474+ if let Some ( index) = & mut group_index {
475+ index. insert ( & item. 0 ) ;
476+ }
473477 group. push ( item) ;
474478 continue ;
475479 }
476480 if is_poolable_read ( & item. 0 ) {
481+ let conflicts = !group. is_empty ( )
482+ && group_index
483+ . get_or_insert_with ( || PendingWriteIndex :: from_group ( & group) )
484+ . conflicts_with_read ( & item. 0 ) ;
485+ if conflicts {
486+ self . flush_write_group ( & mut group, & mut slow_queue) ;
487+ group_index = None ;
488+ }
477489 self . forward_to_read_pool ( item, & mut slow_queue) ;
478490 continue ;
479491 }
480492 self . flush_write_group ( & mut group, & mut slow_queue) ;
493+ group_index = None ;
481494 self . process_single ( item, & mut slow_queue) ;
482495 }
483496 self . flush_write_group ( & mut group, & mut slow_queue) ;
497+ group_index = None ;
484498 }
485499 }
486500
@@ -581,7 +595,29 @@ impl FjallStorage {
581595 return ;
582596 }
583597
584- let members = std:: mem:: take ( group) ;
598+ let mut members = std:: mem:: take ( group) ;
599+ members. retain ( |item| {
600+ if item. 1 . is_disconnected ( ) {
601+ let _guard = item. 2 . enter ( ) ;
602+ warn ! (
603+ event = "storage.request.abandoned" ,
604+ operation = storage_effect_kind( & item. 0 ) ,
605+ "Skipping abandoned storage request"
606+ ) ;
607+ false
608+ } else {
609+ true
610+ }
611+ } ) ;
612+ if members. is_empty ( ) {
613+ return ;
614+ }
615+ if members. len ( ) == 1 {
616+ let item = members. pop ( ) . expect ( "group has one item" ) ;
617+ self . process_single ( item, slow_queue) ;
618+ return ;
619+ }
620+
585621 let service_started = Instant :: now ( ) ;
586622 let tx = match self . buffered_write_tx ( ) {
587623 Ok ( tx) => tx,
@@ -602,43 +638,44 @@ impl FjallStorage {
602638 }
603639 }
604640
605- let commit_result = self
606- . commit_buffered_write_tx ( tx)
607- . and_then ( |( ) | self . persist_journal ( ) ) ;
608-
609- match commit_result {
610- Ok ( ( ) ) => {
611- let service_elapsed = service_started. elapsed ( ) ;
612- for ( ( effect, response_tx, span, enqueued_at) , outcome) in prepared {
613- let _guard = span. enter ( ) ;
614- let queue_wait = enqueued_at. elapsed ( ) . saturating_sub ( service_elapsed) ;
615- let event = match outcome {
616- Ok ( event) => event,
617- Err ( error) => StorageEvent :: Error { error } ,
618- } ;
619- let result = storage_event_kind ( & event) ;
620- span. record ( "queue_wait_ms" , duration_ms ( queue_wait) ) ;
621- span. record ( "service_ms" , duration_ms ( service_elapsed) ) ;
622- span. record ( "result" , result) ;
623- span. record ( "path" , "group_commit" ) ;
624- slow_queue. observe (
625- storage_effect_kind ( & effect) ,
626- storage_effect_key_space ( & effect) ,
627- queue_wait,
628- service_elapsed,
629- result,
630- ) ;
631- if !response_tx. is_disconnected ( ) {
632- response_tx. send ( event) ;
633- }
634- }
635- }
636- Err ( _) => {
637- // Conflict with a held transaction: retry each member alone so
638- // only genuinely conflicting writes fail.
641+ let group_error = match self . commit_buffered_write_tx ( tx) {
642+ Ok ( ( ) ) => self . persist_journal ( ) . err ( ) ,
643+ Err ( StorageError :: TransactionConflict ) => {
639644 for ( item, _) in prepared {
640645 self . process_single ( item, slow_queue) ;
641646 }
647+ return ;
648+ }
649+ Err ( error) => Some ( error) ,
650+ } ;
651+
652+ let service_elapsed = service_started. elapsed ( ) ;
653+ for ( ( effect, response_tx, span, enqueued_at) , outcome) in prepared {
654+ let _guard = span. enter ( ) ;
655+ let queue_wait = enqueued_at. elapsed ( ) . saturating_sub ( service_elapsed) ;
656+ let event = match outcome {
657+ Ok ( event) => match & group_error {
658+ Some ( error) => StorageEvent :: Error {
659+ error : error. clone ( ) ,
660+ } ,
661+ None => event,
662+ } ,
663+ Err ( error) => StorageEvent :: Error { error } ,
664+ } ;
665+ let result = storage_event_kind ( & event) ;
666+ span. record ( "queue_wait_ms" , duration_ms ( queue_wait) ) ;
667+ span. record ( "service_ms" , duration_ms ( service_elapsed) ) ;
668+ span. record ( "result" , result) ;
669+ span. record ( "path" , "group_commit" ) ;
670+ slow_queue. observe (
671+ storage_effect_kind ( & effect) ,
672+ storage_effect_key_space ( & effect) ,
673+ queue_wait,
674+ service_elapsed,
675+ result,
676+ ) ;
677+ if !response_tx. is_disconnected ( ) {
678+ response_tx. send ( event) ;
642679 }
643680 }
644681 }
@@ -664,8 +701,12 @@ impl FjallStorage {
664701 txn_id : None ,
665702 } => {
666703 let mut entries = Vec :: with_capacity ( writes. len ( ) ) ;
704+ let mut resolved = Vec :: with_capacity ( writes. len ( ) ) ;
667705 for ( key_space, key, value) in writes {
668706 let keyspace = self . store . resolve_keyspace ( key_space) ?;
707+ resolved. push ( ( keyspace, key_space, key, value) ) ;
708+ }
709+ for ( keyspace, key_space, key, value) in resolved {
669710 tx. insert ( keyspace, key. clone ( ) , value. clone ( ) ) ;
670711 entries. push ( ( key_space. clone ( ) , key. clone ( ) ) ) ;
671712 }
@@ -685,8 +726,12 @@ impl FjallStorage {
685726 txn_id : None ,
686727 } => {
687728 let mut entries = Vec :: with_capacity ( deletes. len ( ) ) ;
729+ let mut resolved = Vec :: with_capacity ( deletes. len ( ) ) ;
688730 for ( key_space, key) in deletes {
689731 let keyspace = self . store . resolve_keyspace ( key_space) ?;
732+ resolved. push ( ( keyspace, key_space, key) ) ;
733+ }
734+ for ( keyspace, key_space, key) in resolved {
690735 tx. remove ( keyspace, key. clone ( ) ) ;
691736 entries. push ( ( key_space. clone ( ) , key. clone ( ) ) ) ;
692737 }
@@ -1208,6 +1253,189 @@ fn is_poolable_read(effect: &StorageEffect) -> bool {
12081253 )
12091254}
12101255
1256+ #[ derive( Default ) ]
1257+ struct PendingWriteIndex {
1258+ key_spaces : Vec < String > ,
1259+ keys : Vec < PendingWriteKey > ,
1260+ sorted : bool ,
1261+ }
1262+
1263+ struct PendingWriteKey {
1264+ key_space : usize ,
1265+ key : ByteView ,
1266+ }
1267+
1268+ impl PendingWriteIndex {
1269+ fn from_group ( group : & [ EffectHandle ] ) -> Self {
1270+ let mut index = Self {
1271+ key_spaces : Vec :: with_capacity ( group. len ( ) ) ,
1272+ keys : Vec :: with_capacity ( group. len ( ) ) ,
1273+ sorted : true ,
1274+ } ;
1275+ for ( effect, _, _, _) in group {
1276+ index. insert ( effect) ;
1277+ }
1278+ index
1279+ }
1280+
1281+ fn insert ( & mut self , effect : & StorageEffect ) {
1282+ match effect {
1283+ StorageEffect :: Write {
1284+ key_space,
1285+ key,
1286+ txn_id : None ,
1287+ ..
1288+ }
1289+ | StorageEffect :: Delete {
1290+ key_space,
1291+ key,
1292+ txn_id : None ,
1293+ } => self . insert_key ( key_space, key) ,
1294+ StorageEffect :: BatchWrite {
1295+ writes,
1296+ txn_id : None ,
1297+ } => {
1298+ for ( key_space, key, _) in writes {
1299+ self . insert_key ( key_space, key) ;
1300+ }
1301+ }
1302+ StorageEffect :: BatchDelete {
1303+ deletes,
1304+ txn_id : None ,
1305+ } => {
1306+ for ( key_space, key) in deletes {
1307+ self . insert_key ( key_space, key) ;
1308+ }
1309+ }
1310+ _ => { }
1311+ }
1312+ }
1313+
1314+ fn insert_key ( & mut self , key_space : & str , key : & ByteView ) {
1315+ let key_space = self . key_space_index_or_insert ( key_space) ;
1316+ self . keys . push ( PendingWriteKey {
1317+ key_space,
1318+ key : key. clone ( ) ,
1319+ } ) ;
1320+ self . sorted = false ;
1321+ }
1322+
1323+ fn conflicts_with_read ( & mut self , read : & StorageEffect ) -> bool {
1324+ match read {
1325+ StorageEffect :: Read {
1326+ key_space,
1327+ key,
1328+ txn_id : None ,
1329+ } => self . contains_key ( key_space, key) ,
1330+ StorageEffect :: BatchRead {
1331+ reads,
1332+ txn_id : None ,
1333+ } => reads
1334+ . iter ( )
1335+ . any ( |( key_space, key) | self . contains_key ( key_space, key) ) ,
1336+ StorageEffect :: Iter {
1337+ key_space,
1338+ prefix,
1339+ start,
1340+ limit,
1341+ txn_id : None ,
1342+ } => * limit != 0 && self . contains_iter_key ( key_space, prefix. as_ref ( ) , start. as_ref ( ) ) ,
1343+ _ => false ,
1344+ }
1345+ }
1346+
1347+ fn contains_key ( & mut self , key_space : & str , key : & ByteView ) -> bool {
1348+ let Some ( key_space) = self . key_space_index ( key_space) else {
1349+ return false ;
1350+ } ;
1351+ self . sort_keys ( ) ;
1352+ self . keys
1353+ . binary_search_by ( |pending| compare_pending_key ( pending, key_space, key. as_ref ( ) ) )
1354+ . is_ok ( )
1355+ }
1356+
1357+ fn contains_iter_key (
1358+ & mut self ,
1359+ key_space : & str ,
1360+ prefix : Option < & ByteView > ,
1361+ start : Option < & IterStart > ,
1362+ ) -> bool {
1363+ let Some ( key_space) = self . key_space_index ( key_space) else {
1364+ return false ;
1365+ } ;
1366+ self . sort_keys ( ) ;
1367+ let start_index = self
1368+ . keys
1369+ . partition_point ( |pending| pending. key_space < key_space) ;
1370+ let end_index = self
1371+ . keys
1372+ . partition_point ( |pending| pending. key_space <= key_space) ;
1373+ self . keys [ start_index..end_index]
1374+ . iter ( )
1375+ . any ( |pending| iter_may_include_key ( prefix, start, & pending. key ) )
1376+ }
1377+
1378+ fn key_space_index ( & self , key_space : & str ) -> Option < usize > {
1379+ self . key_spaces
1380+ . iter ( )
1381+ . position ( |existing| existing. as_str ( ) == key_space)
1382+ }
1383+
1384+ fn key_space_index_or_insert ( & mut self , key_space : & str ) -> usize {
1385+ match self . key_space_index ( key_space) {
1386+ Some ( index) => index,
1387+ None => {
1388+ self . key_spaces . push ( key_space. to_string ( ) ) ;
1389+ self . key_spaces . len ( ) - 1
1390+ }
1391+ }
1392+ }
1393+
1394+ fn sort_keys ( & mut self ) {
1395+ if self . sorted {
1396+ return ;
1397+ }
1398+ self . keys . sort_unstable_by ( |left, right| {
1399+ left. key_space
1400+ . cmp ( & right. key_space )
1401+ . then_with ( || left. key . as_ref ( ) . cmp ( right. key . as_ref ( ) ) )
1402+ } ) ;
1403+ self . keys . dedup_by ( |left, right| {
1404+ left. key_space == right. key_space && left. key . as_ref ( ) == right. key . as_ref ( )
1405+ } ) ;
1406+ self . sorted = true ;
1407+ }
1408+ }
1409+
1410+ fn compare_pending_key (
1411+ pending : & PendingWriteKey ,
1412+ key_space : usize ,
1413+ key : & [ u8 ] ,
1414+ ) -> std:: cmp:: Ordering {
1415+ pending
1416+ . key_space
1417+ . cmp ( & key_space)
1418+ . then_with ( || pending. key . as_ref ( ) . cmp ( key) )
1419+ }
1420+
1421+ fn iter_may_include_key (
1422+ prefix : Option < & ByteView > ,
1423+ start : Option < & IterStart > ,
1424+ key : & ByteView ,
1425+ ) -> bool {
1426+ let key = key. as_ref ( ) ;
1427+ if let Some ( prefix) = prefix
1428+ && !key. starts_with ( prefix. as_ref ( ) )
1429+ {
1430+ return false ;
1431+ }
1432+ match start {
1433+ Some ( IterStart :: After ( start) ) if key <= start. as_ref ( ) => false ,
1434+ Some ( IterStart :: At ( start) ) if key < start. as_ref ( ) => false ,
1435+ _ => true ,
1436+ }
1437+ }
1438+
12111439fn spawn_read_pool ( store : Store , threads : usize ) -> Vec < EffectSender > {
12121440 let mut senders = Vec :: with_capacity ( threads) ;
12131441 for _ in 0 ..threads {
0 commit comments