huskies: merge 889
This commit is contained in:
@@ -143,6 +143,13 @@ pub fn write_item(
|
||||
return;
|
||||
};
|
||||
|
||||
// Reject any write (insert or update) for a tombstoned story_id.
|
||||
// This prevents a concurrent or late-arriving write from resurrecting
|
||||
// a story that was permanently deleted via evict_item.
|
||||
if state.tombstones.contains(story_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(&idx) = state.index.get(story_id) {
|
||||
// Capture the old stage before updating so we can detect transitions.
|
||||
let old_stage = match state.crdt.doc.items[idx].stage.view() {
|
||||
|
||||
@@ -733,3 +733,84 @@ async fn bug_511_rowid_replay_preserves_field_update_after_list_insert() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Story 889 regression tests ───────────────────────────────────────────────
|
||||
|
||||
/// Regression for story 889: a tombstoned story must not be resurrected by
|
||||
/// concurrent write_item calls racing the delete. Spawns a tokio task that
|
||||
/// hammers write_item every 10ms, tombstones the item mid-race, then verifies
|
||||
/// the projection stays empty for ~500ms and remains empty after the writer
|
||||
/// stops.
|
||||
///
|
||||
/// The tokio current_thread runtime keeps all tasks on the same OS thread, so
|
||||
/// the thread-local test CRDT is visible to the spawned task.
|
||||
#[tokio::test]
|
||||
async fn tombstone_survives_concurrent_writes() {
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use super::super::read::{evict_item, read_item};
|
||||
|
||||
init_for_test();
|
||||
|
||||
let story_id = "889_story_tombstone_concurrent";
|
||||
|
||||
write_item(
|
||||
story_id,
|
||||
"2_current",
|
||||
Some("Tombstone Concurrent Test"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(
|
||||
read_item(story_id).is_some(),
|
||||
"item must exist before eviction"
|
||||
);
|
||||
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let stop_clone = stop.clone();
|
||||
|
||||
let writer = tokio::task::spawn(async move {
|
||||
while !stop_clone.load(Ordering::Relaxed) {
|
||||
write_item(
|
||||
story_id,
|
||||
"2_current",
|
||||
Some("Tombstone Concurrent Test"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
});
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(30)).await;
|
||||
|
||||
evict_item(story_id).expect("evict_item must succeed");
|
||||
|
||||
let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(500);
|
||||
while tokio::time::Instant::now() < deadline {
|
||||
assert!(
|
||||
read_item(story_id).is_none(),
|
||||
"tombstoned story must not reappear while concurrent writes are in flight"
|
||||
);
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
writer.await.unwrap();
|
||||
|
||||
assert!(
|
||||
read_item(story_id).is_none(),
|
||||
"tombstoned story must stay gone after concurrent writer stops"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user