Skip to content

Commit 6560dff

Browse files
committed
Keep insert-or-update cache unchanged on persist failure
Build the updated object separately and persist it before replacing the cached entry, so failed writes leave memory aligned with storage. This finding was discovered by Project Loupe AI-Assisted-By: OpenAI Codex
1 parent 8a54260 commit 6560dff

1 file changed

Lines changed: 83 additions & 19 deletions

File tree

src/data_store.rs

Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
66
// accordance with one or both of these licenses.
77

8-
use std::collections::{hash_map, HashMap};
8+
use std::collections::HashMap;
99
use std::ops::Deref;
1010
use std::sync::{Arc, Mutex};
1111

@@ -83,28 +83,32 @@ where
8383

8484
pub(crate) async fn insert_or_update(&self, object: SO) -> Result<bool, Error> {
8585
let _guard = self.mutation_lock.lock().await;
86-
let (updated, data_to_persist) = {
87-
let mut locked_objects = self.objects.lock().expect("lock");
88-
match locked_objects.entry(object.id()) {
89-
hash_map::Entry::Occupied(mut e) => {
90-
let update = object.to_update();
91-
let updated = e.get_mut().update(update);
92-
let data_to_persist =
93-
if updated { Some(Self::encode_object(e.get())) } else { None };
94-
(updated, data_to_persist)
95-
},
96-
hash_map::Entry::Vacant(e) => {
97-
let data_to_persist = Self::encode_object(&object);
98-
e.insert(object);
99-
(true, Some(data_to_persist))
100-
},
86+
87+
let id = object.id();
88+
let data_to_persist = {
89+
let locked_objects = self.objects.lock().expect("lock");
90+
if let Some(existing_object) = locked_objects.get(&id) {
91+
let mut updated_object = existing_object.clone();
92+
let updated = updated_object.update(object.to_update());
93+
if updated {
94+
Some(updated_object)
95+
} else {
96+
None
97+
}
98+
} else {
99+
Some(object)
101100
}
102101
};
103102

104-
if let Some((store_key, data)) = data_to_persist {
105-
self.persist_encoded(store_key, data).await?;
103+
match data_to_persist {
104+
Some(updated_object) => {
105+
self.persist(&updated_object).await?;
106+
let mut locked_objects = self.objects.lock().expect("lock");
107+
locked_objects.insert(id, updated_object);
108+
Ok(true)
109+
},
110+
None => Ok(false),
106111
}
107-
Ok(updated)
108112
}
109113

110114
pub(crate) async fn remove(&self, id: &SO::Id) -> Result<(), Error> {
@@ -219,6 +223,7 @@ where
219223
#[cfg(test)]
220224
mod tests {
221225
use lightning::impl_writeable_tlv_based;
226+
use lightning::io;
222227
use lightning::util::test_utils::TestLogger;
223228

224229
use super::*;
@@ -281,6 +286,46 @@ mod tests {
281286
(2, data, required),
282287
});
283288

289+
struct FailingStore;
290+
291+
impl KVStore for FailingStore {
292+
fn read(
293+
&self, _primary_namespace: &str, _secondary_namespace: &str, _key: &str,
294+
) -> impl std::future::Future<Output = Result<Vec<u8>, io::Error>> + 'static + Send {
295+
async { Err(io::Error::new(io::ErrorKind::Other, "read failed")) }
296+
}
297+
298+
fn write(
299+
&self, _primary_namespace: &str, _secondary_namespace: &str, _key: &str, _buf: Vec<u8>,
300+
) -> impl std::future::Future<Output = Result<(), io::Error>> + 'static + Send {
301+
async { Err(io::Error::new(io::ErrorKind::Other, "write failed")) }
302+
}
303+
304+
fn remove(
305+
&self, _primary_namespace: &str, _secondary_namespace: &str, _key: &str, _lazy: bool,
306+
) -> impl std::future::Future<Output = Result<(), io::Error>> + 'static + Send {
307+
async { Err(io::Error::new(io::ErrorKind::Other, "remove failed")) }
308+
}
309+
310+
fn list(
311+
&self, _primary_namespace: &str, _secondary_namespace: &str,
312+
) -> impl std::future::Future<Output = Result<Vec<String>, io::Error>> + 'static + Send {
313+
async { Err(io::Error::new(io::ErrorKind::Other, "list failed")) }
314+
}
315+
}
316+
317+
fn new_failing_data_store(objects: Vec<TestObject>) -> DataStore<TestObject, Arc<TestLogger>> {
318+
let store: Arc<DynStore> = Arc::new(DynStoreWrapper(FailingStore));
319+
let logger = Arc::new(TestLogger::new());
320+
DataStore::new(
321+
objects,
322+
"datastore_test_primary".to_string(),
323+
"datastore_test_secondary".to_string(),
324+
store,
325+
logger,
326+
)
327+
}
328+
284329
#[tokio::test]
285330
async fn data_is_persisted() {
286331
let store: Arc<DynStore> = Arc::new(DynStoreWrapper(InMemoryStore::new()));
@@ -346,4 +391,23 @@ mod tests {
346391
new_iou_object.data[0] += 1;
347392
assert_eq!(Ok(true), data_store.insert_or_update(new_iou_object).await);
348393
}
394+
395+
#[tokio::test]
396+
async fn insert_or_update_does_not_mutate_memory_if_persist_fails() {
397+
let existing_id = TestObjectId { id: [42u8; 4] };
398+
let existing_object = TestObject { id: existing_id, data: [23u8; 3] };
399+
let data_store = new_failing_data_store(vec![existing_object]);
400+
401+
let updated_object = TestObject { id: existing_id, data: [24u8; 3] };
402+
assert_eq!(
403+
Err(Error::PersistenceFailed),
404+
data_store.insert_or_update(updated_object).await
405+
);
406+
assert_eq!(Some(existing_object), data_store.get(&existing_id));
407+
408+
let new_id = TestObjectId { id: [55u8; 4] };
409+
let new_object = TestObject { id: new_id, data: [34u8; 3] };
410+
assert_eq!(Err(Error::PersistenceFailed), data_store.insert_or_update(new_object).await);
411+
assert!(data_store.get(&new_id).is_none());
412+
}
349413
}

0 commit comments

Comments
 (0)