Skip to content

Commit f403ba4

Browse files
committed
fix: Read after write in batch, errors on abort
1 parent 8dea59a commit f403ba4

2 files changed

Lines changed: 264 additions & 36 deletions

File tree

core/src/errors.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ pub enum SourceConnectorResolutionError {
100100
ResolveFailed,
101101
}
102102

103-
#[derive(Debug, Error, PartialEq)]
103+
#[derive(Debug, Error, PartialEq, Clone)]
104104
pub enum StorageError {
105105
#[error("Key not found")]
106106
KeyNotFound,

storage/src/storage.rs

Lines changed: 263 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
12111439
fn 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

Comments
 (0)