Skip to content

Commit 8dfa964

Browse files
committed
fixup! Implement tiered storage
Here we serialize per-key writes in TierStore to prevent out-of-order backup updates. Without serialization, concurrent writes to the same key can interleave across the primary and backup stores (e.g. [w1.primary] → [w2.primary] → [w2.backup] → [w1.backup]), leaving the backup with stale data. By adding a per-key TokioMutex that serializes write and remove operations for a given (namespace, key) tuple, we ensure both the primary and backup stores reflect the same final value. The lock map is cleaned up after each operation when no other in-flight operations hold a reference, following the same pattern used in VssStore.
1 parent 15c5cf3 commit 8dfa964

1 file changed

Lines changed: 67 additions & 9 deletions

File tree

src/io/tier_store.rs

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,45 @@ struct TierStoreInner {
140140
ephemeral_store: Option<Arc<DynStore>>,
141141
/// An optional second durable store for primary-backed data.
142142
backup_store: Option<Arc<DynStore>>,
143+
/// Per-key locks for serializing primary+backup operations.
144+
locks: Mutex<HashMap<String, Arc<TokioMutex<()>>>>,
143145
logger: Arc<Logger>,
144146
}
145147

146148
impl TierStoreInner {
147149
/// Creates a tier store with the primary data store.
148150
pub fn new(primary_store: Arc<DynStore>, logger: Arc<Logger>) -> Self {
149-
Self { primary_store, ephemeral_store: None, backup_store: None, logger }
151+
Self {
152+
primary_store,
153+
ephemeral_store: None,
154+
backup_store: None,
155+
locks: Mutex::new(HashMap::new()),
156+
logger,
157+
}
158+
}
159+
160+
fn get_key_lock(&self, locking_key: String) -> Arc<TokioMutex<()>> {
161+
let mut locks = self.locks.lock().expect("lock");
162+
Arc::clone(locks.entry(locking_key).or_default())
163+
}
164+
165+
fn clean_locks(&self, lock_ref: &Arc<TokioMutex<()>>, locking_key: String) {
166+
let mut locks = self.locks.lock().expect("lock");
167+
let strong_count = Arc::strong_count(lock_ref);
168+
debug_assert!(strong_count >= 2, "Unexpected TierStore lock strong count");
169+
if strong_count == 2 {
170+
locks.remove(&locking_key);
171+
}
172+
}
173+
174+
fn build_locking_key(
175+
&self, primary_namespace: &str, secondary_namespace: &str, key: &str,
176+
) -> String {
177+
if primary_namespace.is_empty() {
178+
key.to_owned()
179+
} else {
180+
format!("{}#{}#{}", primary_namespace, secondary_namespace, key)
181+
}
150182
}
151183

152184
/// Reads from the primary data store.
@@ -317,13 +349,26 @@ impl TierStoreInner {
317349
}
318350
}
319351

320-
self.write_primary_backup_async(
352+
let locking_key = self.build_locking_key(
321353
primary_namespace.as_str(),
322354
secondary_namespace.as_str(),
323355
key.as_str(),
324-
buf,
325-
)
326-
.await
356+
);
357+
let key_lock = self.get_key_lock(locking_key.clone());
358+
359+
let res = {
360+
let _guard = key_lock.lock().await;
361+
self.write_primary_backup_async(
362+
primary_namespace.as_str(),
363+
secondary_namespace.as_str(),
364+
key.as_str(),
365+
buf,
366+
)
367+
.await
368+
};
369+
370+
self.clean_locks(&key_lock, locking_key);
371+
res
327372
}
328373

329374
async fn remove_internal(
@@ -349,13 +394,26 @@ impl TierStoreInner {
349394
}
350395
}
351396

352-
self.remove_primary_backup_async(
397+
let locking_key = self.build_locking_key(
353398
primary_namespace.as_str(),
354399
secondary_namespace.as_str(),
355400
key.as_str(),
356-
lazy,
357-
)
358-
.await
401+
);
402+
let key_lock = self.get_key_lock(locking_key.clone());
403+
404+
let res = {
405+
let _guard = key_lock.lock().await;
406+
self.remove_primary_backup_async(
407+
primary_namespace.as_str(),
408+
secondary_namespace.as_str(),
409+
key.as_str(),
410+
lazy,
411+
)
412+
.await
413+
};
414+
415+
self.clean_locks(&key_lock, locking_key);
416+
res
359417
}
360418

361419
async fn list_internal(

0 commit comments

Comments
 (0)