Skip to content

Commit 944f636

Browse files
committed
Isolate TierStore mutations by key
Preparing an entire namespace before every write or removal allows one unrecoverable journal entry to block mutations of unrelated keys sharing that namespace. In this commit, we: - Initialize the ordering index before mutations, then recover journal state and reconcile cache placement for only the requested key while holding its existing operation lock. - Reserve complete namespace recovery and cache reconciliation for listing operations. - Remove the obsolete journal-list snapshot gate and add coverage showing that a stuck key does not block unrelated writes or removals, while listings retain their deliberate fail-fast behavior. - Verify that touching one cache key no longer migrates another cache key in the same namespace. Assisted-by: Amp (AI coding agent)
1 parent b7d43f4 commit 944f636

1 file changed

Lines changed: 135 additions & 83 deletions

File tree

src/io/tier_store.rs

Lines changed: 135 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -940,13 +940,7 @@ impl TierStoreInner {
940940
let lock_ref = self.get_lock_ref(locking_key.clone());
941941
let result: io::Result<Vec<u8>> = async {
942942
let _guard = lock_ref.lock().await;
943-
self.recover_key_locked(&primary_namespace, &secondary_namespace, &key).await?;
944-
self.reconcile_ephemeral_cache_key_locked(
945-
&primary_namespace,
946-
&secondary_namespace,
947-
&key,
948-
)
949-
.await?;
943+
self.prepare_key_locked(&primary_namespace, &secondary_namespace, &key).await?;
950944

951945
if is_ephemeral_cached_key(&primary_namespace, &secondary_namespace, &key) {
952946
if let Some(eph_store) = self.ephemeral_store.as_ref() {
@@ -979,19 +973,19 @@ impl TierStoreInner {
979973
Some(key.as_str()),
980974
"write",
981975
)?;
982-
self.prepare_namespace(&primary_namespace, &secondary_namespace).await?;
976+
self.ensure_namespace_indexed(&primary_namespace, &secondary_namespace).await?;
983977

984978
self.execute_locked_write(lock_ref, locking_key, version, || async move {
985979
self.write_locked(&primary_namespace, &secondary_namespace, &key, buf).await
986980
})
987981
.await
988982
}
989983

990-
/// Writes one key after recovering any pending operation while its per-key lock is held.
984+
/// Prepares and writes one key while its per-key operation lock is held.
991985
async fn write_locked(
992986
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, value: Vec<u8>,
993987
) -> io::Result<()> {
994-
self.recover_key_locked(primary_namespace, secondary_namespace, key).await?;
988+
self.prepare_key_locked(primary_namespace, secondary_namespace, key).await?;
995989
let tier = self.value_tier(primary_namespace, secondary_namespace, key);
996990
let Some(index) = self.index.as_ref() else {
997991
return self
@@ -1034,19 +1028,19 @@ impl TierStoreInner {
10341028
Some(key.as_str()),
10351029
"remove",
10361030
)?;
1037-
self.prepare_namespace(&primary_namespace, &secondary_namespace).await?;
1031+
self.ensure_namespace_indexed(&primary_namespace, &secondary_namespace).await?;
10381032

10391033
self.execute_locked_write(lock_ref, locking_key, version, || async move {
10401034
self.remove_locked(&primary_namespace, &secondary_namespace, &key, lazy).await
10411035
})
10421036
.await
10431037
}
10441038

1045-
/// Removes one key after recovering any pending operation while its per-key lock is held.
1039+
/// Prepares and removes one key while its per-key operation lock is held.
10461040
async fn remove_locked(
10471041
&self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool,
10481042
) -> io::Result<()> {
1049-
self.recover_key_locked(primary_namespace, secondary_namespace, key).await?;
1043+
self.prepare_key_locked(primary_namespace, secondary_namespace, key).await?;
10501044
let tier = self.value_tier(primary_namespace, secondary_namespace, key);
10511045
let Some(index) = self.index.as_ref() else {
10521046
return self
@@ -1220,7 +1214,7 @@ impl TierStoreInner {
12201214
"list",
12211215
)?;
12221216

1223-
self.prepare_namespace(&primary_namespace, &secondary_namespace).await?;
1217+
self.prepare_namespace_for_listing(&primary_namespace, &secondary_namespace).await?;
12241218
if let Some(index) = self.index.as_ref() {
12251219
return index.list(&primary_namespace, &secondary_namespace).await;
12261220
}
@@ -1281,8 +1275,8 @@ impl TierStoreInner {
12811275
result
12821276
}
12831277

1284-
/// Initializes a namespace, recovers pending membership changes, and reconciles cache placement.
1285-
async fn prepare_namespace(
1278+
/// Prepares a complete namespace before exposing its index through a listing.
1279+
async fn prepare_namespace_for_listing(
12861280
&self, primary_namespace: &str, secondary_namespace: &str,
12871281
) -> io::Result<()> {
12881282
self.ensure_namespace_indexed(primary_namespace, secondary_namespace).await?;
@@ -1323,13 +1317,7 @@ impl TierStoreInner {
13231317
let lock_ref = self.get_lock_ref(locking_key.clone());
13241318
let result: io::Result<()> = async {
13251319
let _guard = lock_ref.lock().await;
1326-
self.recover_key_locked(primary_namespace, secondary_namespace, key).await?;
1327-
self.reconcile_ephemeral_cache_key_locked(
1328-
primary_namespace,
1329-
secondary_namespace,
1330-
key,
1331-
)
1332-
.await
1320+
self.prepare_key_locked(primary_namespace, secondary_namespace, key).await
13331321
}
13341322
.await;
13351323
self.clean_locks(&lock_ref, locking_key);
@@ -1406,6 +1394,14 @@ impl TierStoreInner {
14061394
index.mark_cache_ready(primary_namespace, secondary_namespace, key).await
14071395
}
14081396

1397+
/// Recovers and reconciles one key while its per-key operation lock is held by the caller.
1398+
async fn prepare_key_locked(
1399+
&self, primary_namespace: &str, secondary_namespace: &str, key: &str,
1400+
) -> io::Result<()> {
1401+
self.recover_key_locked(primary_namespace, secondary_namespace, key).await?;
1402+
self.reconcile_ephemeral_cache_key_locked(primary_namespace, secondary_namespace, key).await
1403+
}
1404+
14091405
/// Completes journaled creates and removals before exposing a namespace.
14101406
async fn recover_namespace(
14111407
&self, primary_namespace: &str, secondary_namespace: &str,
@@ -1495,7 +1491,7 @@ impl TierStoreInner {
14951491
"list_paginated",
14961492
)?;
14971493

1498-
self.prepare_namespace(&primary_namespace, &secondary_namespace).await?;
1494+
self.prepare_namespace_for_listing(&primary_namespace, &secondary_namespace).await?;
14991495
if let Some(index) = self.index.as_ref() {
15001496
return index
15011497
.list_paginated(&primary_namespace, &secondary_namespace, page_token)
@@ -1582,7 +1578,7 @@ mod tests {
15821578
use std::panic::RefUnwindSafe;
15831579
use std::path::PathBuf;
15841580
use std::sync::atomic::{AtomicUsize, Ordering};
1585-
use std::sync::{Arc, Mutex};
1581+
use std::sync::Arc;
15861582

15871583
use lightning::util::logger::Level;
15881584
use lightning::util::persist::{
@@ -1593,7 +1589,6 @@ mod tests {
15931589
NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, SCORER_PERSISTENCE_SECONDARY_NAMESPACE,
15941590
};
15951591
use lightning_persister::fs_store::v2::FilesystemStoreV2;
1596-
use tokio::sync::oneshot;
15971592

15981593
use super::*;
15991594
use crate::io::test_utils::{
@@ -2144,6 +2139,80 @@ mod tests {
21442139
.unwrap());
21452140
}
21462141

2142+
#[tokio::test]
2143+
async fn writing_one_cache_key_only_reconciles_that_key() {
2144+
let base_dir = random_storage_path();
2145+
let log_path = base_dir.join("tier_store_test.log").to_string_lossy().into_owned();
2146+
let logger = Arc::new(Logger::new_fs_writer(log_path, Level::Trace).unwrap());
2147+
let _cleanup = CleanupDir(base_dir);
2148+
2149+
let primary_store: Arc<DynStore> = Arc::new(DynStoreWrapper(InMemoryStore::new()));
2150+
let ephemeral_store: Arc<DynStore> = Arc::new(DynStoreWrapper(InMemoryStore::new()));
2151+
for (key, value) in
2152+
[(SCORER_PERSISTENCE_KEY, vec![1]), (EXTERNAL_PATHFINDING_SCORES_CACHE_KEY, vec![2])]
2153+
{
2154+
primary_store
2155+
.write(
2156+
SCORER_PERSISTENCE_PRIMARY_NAMESPACE,
2157+
SCORER_PERSISTENCE_SECONDARY_NAMESPACE,
2158+
key,
2159+
value,
2160+
)
2161+
.await
2162+
.unwrap();
2163+
}
2164+
let mut tier = setup_tier_store(Arc::clone(&primary_store), logger);
2165+
set_test_index_store(&mut tier);
2166+
tier.set_ephemeral_store(Arc::clone(&ephemeral_store));
2167+
2168+
tier.write(
2169+
SCORER_PERSISTENCE_PRIMARY_NAMESPACE,
2170+
SCORER_PERSISTENCE_SECONDARY_NAMESPACE,
2171+
SCORER_PERSISTENCE_KEY,
2172+
vec![3],
2173+
)
2174+
.await
2175+
.unwrap();
2176+
assert!(primary_store
2177+
.read(
2178+
SCORER_PERSISTENCE_PRIMARY_NAMESPACE,
2179+
SCORER_PERSISTENCE_SECONDARY_NAMESPACE,
2180+
SCORER_PERSISTENCE_KEY,
2181+
)
2182+
.await
2183+
.is_err());
2184+
assert_eq!(
2185+
ephemeral_store
2186+
.read(
2187+
SCORER_PERSISTENCE_PRIMARY_NAMESPACE,
2188+
SCORER_PERSISTENCE_SECONDARY_NAMESPACE,
2189+
SCORER_PERSISTENCE_KEY,
2190+
)
2191+
.await
2192+
.unwrap(),
2193+
vec![3]
2194+
);
2195+
assert!(ephemeral_store
2196+
.read(
2197+
SCORER_PERSISTENCE_PRIMARY_NAMESPACE,
2198+
SCORER_PERSISTENCE_SECONDARY_NAMESPACE,
2199+
EXTERNAL_PATHFINDING_SCORES_CACHE_KEY,
2200+
)
2201+
.await
2202+
.is_err());
2203+
assert_eq!(
2204+
primary_store
2205+
.read(
2206+
SCORER_PERSISTENCE_PRIMARY_NAMESPACE,
2207+
SCORER_PERSISTENCE_SECONDARY_NAMESPACE,
2208+
EXTERNAL_PATHFINDING_SCORES_CACHE_KEY,
2209+
)
2210+
.await
2211+
.unwrap(),
2212+
vec![2]
2213+
);
2214+
}
2215+
21472216
#[tokio::test]
21482217
async fn namespace_initialization_preserves_existing_primary_order_across_pages() {
21492218
let base_dir = random_storage_path();
@@ -2336,6 +2405,40 @@ mod tests {
23362405
assert!(tier.read("namespace", "", "stuck").await.is_err());
23372406
}
23382407

2408+
#[tokio::test]
2409+
async fn writes_and_removals_recover_only_the_requested_key() {
2410+
let base_dir = random_storage_path();
2411+
let log_path = base_dir.join("tier_store_test.log").to_string_lossy().into_owned();
2412+
let logger = Arc::new(Logger::new_fs_writer(log_path, Level::Trace).unwrap());
2413+
let _cleanup = CleanupDir(base_dir);
2414+
2415+
let primary_store: Arc<DynStore> = Arc::new(DynStoreWrapper(InMemoryStore::new()));
2416+
for (key, value) in [("stuck", vec![1]), ("writable", vec![2]), ("removable", vec![3])] {
2417+
primary_store.write("namespace", "", key, value).await.unwrap();
2418+
}
2419+
let mut tier = setup_tier_store(Arc::clone(&primary_store), logger);
2420+
set_test_index_store(&mut tier);
2421+
tier.inner.ensure_namespace_indexed("namespace", "").await.unwrap();
2422+
let pending = JournalEntry {
2423+
primary_namespace: "namespace".to_string(),
2424+
secondary_namespace: String::new(),
2425+
key: "stuck".to_string(),
2426+
tier: ValueTier::Primary,
2427+
requires_backup: true,
2428+
operation: JournalOperation::Remove { lazy: false },
2429+
};
2430+
let index = tier.inner.index.as_ref().unwrap();
2431+
index.write_journal_entry(&pending).await.unwrap();
2432+
2433+
tier.write("namespace", "", "writable", vec![4]).await.unwrap();
2434+
tier.remove("namespace", "", "removable", false).await.unwrap();
2435+
2436+
assert_eq!(primary_store.read("namespace", "", "writable").await.unwrap(), vec![4]);
2437+
assert!(primary_store.read("namespace", "", "removable").await.is_err());
2438+
assert!(index.read_journal_entry("namespace", "", "stuck").await.is_ok());
2439+
assert!(KVStore::list(&tier, "namespace", "").await.is_err());
2440+
}
2441+
23392442
#[tokio::test]
23402443
async fn read_recovers_pending_removal_before_returning_value() {
23412444
let base_dir = random_storage_path();
@@ -2478,34 +2581,18 @@ mod tests {
24782581
}
24792582

24802583
#[tokio::test]
2481-
async fn queued_write_recovers_pending_create_under_key_lock_before_classifying() {
2584+
async fn write_recovers_pending_create_under_key_lock_before_classifying() {
24822585
let base_dir = random_storage_path();
24832586
let log_path = base_dir.join("tier_store_test.log").to_string_lossy().into_owned();
24842587
let logger = Arc::new(Logger::new_fs_writer(log_path, Level::Trace).unwrap());
24852588
let _cleanup = CleanupDir(base_dir);
24862589

24872590
let primary_store: Arc<DynStore> = Arc::new(DynStoreWrapper(InMemoryStore::new()));
2488-
let (snapshot_taken_tx, snapshot_taken_rx) = oneshot::channel();
2489-
let (allow_return_tx, allow_return_rx) = oneshot::channel();
2490-
let index_store: Arc<DynStore> =
2491-
Arc::new(DynStoreWrapper(InstrumentedStore::new(StoreBehavior::GateJournalList {
2492-
snapshot_taken: Mutex::new(Some(snapshot_taken_tx)),
2493-
allow_return: Mutex::new(Some(allow_return_rx)),
2494-
})));
2591+
let index_store: Arc<DynStore> = Arc::new(DynStoreWrapper(InMemoryStore::new()));
24952592
let mut tier = setup_tier_store(Arc::clone(&primary_store), logger);
24962593
tier.set_index_store(TierStoreIndex::from_store(index_store));
24972594
tier.inner.ensure_namespace_indexed("namespace", "").await.unwrap();
24982595

2499-
let tier = Arc::new(tier);
2500-
let locking_key = tier.inner.build_locking_key("namespace", "", "key");
2501-
let lock_ref = tier.inner.get_lock_ref(locking_key);
2502-
let guard = lock_ref.lock().await;
2503-
let write_task = {
2504-
let tier = Arc::clone(&tier);
2505-
tokio::spawn(async move { tier.write("namespace", "", "key", vec![2]).await })
2506-
};
2507-
snapshot_taken_rx.await.unwrap();
2508-
25092596
let pending = JournalEntry {
25102597
primary_namespace: "namespace".to_string(),
25112598
secondary_namespace: String::new(),
@@ -2517,11 +2604,8 @@ mod tests {
25172604
let index = tier.inner.index.as_ref().unwrap();
25182605
index.write_journal_entry(&pending).await.unwrap();
25192606
primary_store.write("namespace", "", "key", vec![1]).await.unwrap();
2520-
allow_return_tx.send(()).unwrap();
2521-
drop(guard);
2522-
drop(lock_ref);
25232607

2524-
write_task.await.unwrap().unwrap();
2608+
tier.write("namespace", "", "key", vec![2]).await.unwrap();
25252609
assert_eq!(primary_store.read("namespace", "", "key").await.unwrap(), vec![2]);
25262610
assert!(index.contains_entry("namespace", "", "key").await.unwrap());
25272611
assert!(index.read_journal_entry("namespace", "", "key").await.is_err());
@@ -2578,14 +2662,8 @@ mod tests {
25782662

25792663
enum StoreBehavior {
25802664
FailList,
2581-
FailWrite {
2582-
attempts: Arc<AtomicUsize>,
2583-
},
2665+
FailWrite { attempts: Arc<AtomicUsize> },
25842666
FailRemove,
2585-
GateJournalList {
2586-
snapshot_taken: Mutex<Option<oneshot::Sender<()>>>,
2587-
allow_return: Mutex<Option<oneshot::Receiver<()>>>,
2588-
},
25892667
}
25902668

25912669
/// A store that injects selected failures or synchronization points while delegating other
@@ -2648,9 +2726,7 @@ mod tests {
26482726
) -> impl Future<Output = Result<Vec<String>, io::Error>> + 'static + Send {
26492727
let list = match &self.behavior {
26502728
StoreBehavior::FailList => None,
2651-
StoreBehavior::FailWrite { .. }
2652-
| StoreBehavior::FailRemove
2653-
| StoreBehavior::GateJournalList { .. } => {
2729+
StoreBehavior::FailWrite { .. } | StoreBehavior::FailRemove => {
26542730
Some(KVStore::list(&self.inner, primary_namespace, secondary_namespace))
26552731
},
26562732
};
@@ -2669,23 +2745,6 @@ mod tests {
26692745
page_token: Option<PageToken>,
26702746
) -> impl Future<Output = Result<PaginatedListResponse, io::Error>> + 'static + Send {
26712747
let fails = matches!(&self.behavior, StoreBehavior::FailList);
2672-
let gate = match &self.behavior {
2673-
StoreBehavior::GateJournalList { snapshot_taken, allow_return } => {
2674-
if primary_namespace == INDEX_JOURNAL_PRIMARY_NAMESPACE {
2675-
let snapshot_taken = snapshot_taken.lock().unwrap().take();
2676-
let allow_return = allow_return.lock().unwrap().take();
2677-
match (snapshot_taken, allow_return) {
2678-
(Some(snapshot_taken), Some(allow_return)) => {
2679-
Some((snapshot_taken, allow_return))
2680-
},
2681-
_ => None,
2682-
}
2683-
} else {
2684-
None
2685-
}
2686-
},
2687-
_ => None,
2688-
};
26892748
let list = PaginatedKVStore::list_paginated(
26902749
&self.inner,
26912750
primary_namespace,
@@ -2696,14 +2755,7 @@ mod tests {
26962755
if fails {
26972756
return Err(io::Error::new(io::ErrorKind::Other, "list_paginated failed"));
26982757
}
2699-
let response = list.await?;
2700-
if let Some((snapshot_taken, allow_return)) = gate {
2701-
let _ = snapshot_taken.send(());
2702-
allow_return.await.map_err(|_| {
2703-
io::Error::new(io::ErrorKind::Other, "journal-list gate was dropped")
2704-
})?;
2705-
};
2706-
Ok(response)
2758+
list.await
27072759
}
27082760
}
27092761
}

0 commit comments

Comments
 (0)