huskies: merge 1098 bug Shadow drift: set_retry_count / bump_retry_count write CRDT register without updating pipeline_items.retry_count
This commit is contained in:
+103
-6
@@ -592,6 +592,42 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// `shadow_write::init` spawns its background task on the calling runtime,
|
||||
/// which under `#[tokio::test]` is per-test and dies when the test ends.
|
||||
/// Park the init on a leaked multi-thread runtime so the bg task lives for
|
||||
/// the whole test process; mirrors `db::ops::tests::ensure_shadow_db`.
|
||||
#[cfg(test)]
|
||||
static SHADOW_RT: std::sync::OnceLock<tokio::runtime::Runtime> = std::sync::OnceLock::new();
|
||||
|
||||
#[cfg(test)]
|
||||
async fn ensure_shadow_db() {
|
||||
static INIT: std::sync::OnceLock<()> = std::sync::OnceLock::new();
|
||||
if INIT.get().is_some() {
|
||||
return;
|
||||
}
|
||||
let rt = SHADOW_RT.get_or_init(|| {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(1)
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("shadow rt")
|
||||
});
|
||||
rt.spawn(async {
|
||||
static INNER: std::sync::OnceLock<()> = std::sync::OnceLock::new();
|
||||
if INNER.get().is_some() {
|
||||
return;
|
||||
}
|
||||
let tmp = tempfile::tempdir().expect("tmp");
|
||||
let db_path = tmp.path().join("pipeline.db");
|
||||
std::mem::forget(tmp);
|
||||
shadow_write::init(&db_path).await.expect("shadow init");
|
||||
let _ = INNER.set(());
|
||||
})
|
||||
.await
|
||||
.expect("shadow init task");
|
||||
let _ = INIT.set(());
|
||||
}
|
||||
|
||||
/// Regression for story 1095: `set_name` must propagate the new name to the
|
||||
/// SQLite shadow table via `sync_item_name`. Before the fix, the CRDT
|
||||
/// register was updated but `pipeline_items.name` stayed stale.
|
||||
@@ -599,10 +635,7 @@ mod tests {
|
||||
async fn set_name_updates_shadow_name_column() {
|
||||
crate::crdt_state::init_for_test();
|
||||
ensure_content_store();
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let db_path = tmp.path().join("pipeline.db");
|
||||
shadow_write::init(&db_path).await.expect("db init");
|
||||
ensure_shadow_db().await;
|
||||
|
||||
let story_id = "9095_story_set_name_shadow";
|
||||
write_item_with_content(
|
||||
@@ -612,17 +645,29 @@ mod tests {
|
||||
ItemMeta::named("Original Name"),
|
||||
);
|
||||
|
||||
// Wait for the initial insert to land.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
|
||||
// Rename via the CRDT setter — now also triggers sync_item_name.
|
||||
crate::crdt_state::set_name(story_id, Some("Updated Name"));
|
||||
|
||||
// Wait for the background write task to flush.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
|
||||
let pool = shadow_write::get_shared_pool().expect("pool must be initialised");
|
||||
// Open a fresh pool on this test's runtime — sqlx pools are not safe
|
||||
// to share across runtimes, so we can't reuse `get_shared_pool()`
|
||||
// (which was created on the leaked shadow-write runtime).
|
||||
let path = shadow_write::SHADOW_DB_PATH
|
||||
.get()
|
||||
.expect("SHADOW_DB_PATH set by init");
|
||||
let opts = sqlx::sqlite::SqliteConnectOptions::new()
|
||||
.filename(path)
|
||||
.create_if_missing(false);
|
||||
let pool = sqlx::SqlitePool::connect_with(opts).await.unwrap();
|
||||
let row: (Option<String>,) =
|
||||
sqlx::query_as("SELECT name FROM pipeline_items WHERE id = ?1")
|
||||
.bind(story_id)
|
||||
.fetch_one(pool)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -633,6 +678,58 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Bug 1098: `bump_retry_count` must mirror the new value to the SQLite
|
||||
/// shadow table, not only to the CRDT register.
|
||||
///
|
||||
/// Before the fix, calling `bump_retry_count` updated the CRDT but left
|
||||
/// `pipeline_items.retry_count` stale.
|
||||
#[tokio::test]
|
||||
async fn bump_retry_count_updates_shadow_table() {
|
||||
crate::crdt_state::init_for_test();
|
||||
ensure_content_store();
|
||||
ensure_shadow_db().await;
|
||||
|
||||
let story_id = "9899_story_retry_shadow_1098";
|
||||
|
||||
// Insert the story into both CRDT and the shadow table.
|
||||
write_item_with_content(
|
||||
story_id,
|
||||
"2_current",
|
||||
"# Retry shadow test\n",
|
||||
ItemMeta::named("Retry Shadow Test"),
|
||||
);
|
||||
|
||||
// Let the background write task process the initial insert.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
|
||||
// Three bumps → retry_count must reach 3 in SQLite.
|
||||
crate::crdt_state::bump_retry_count(story_id);
|
||||
crate::crdt_state::bump_retry_count(story_id);
|
||||
crate::crdt_state::bump_retry_count(story_id);
|
||||
|
||||
// Let the background write task process all three updates.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
|
||||
let path = shadow_write::SHADOW_DB_PATH
|
||||
.get()
|
||||
.expect("SHADOW_DB_PATH set by init");
|
||||
let opts = sqlx::sqlite::SqliteConnectOptions::new()
|
||||
.filename(path)
|
||||
.create_if_missing(false);
|
||||
let pool = sqlx::SqlitePool::connect_with(opts).await.unwrap();
|
||||
let (count,): (i64,) =
|
||||
sqlx::query_as("SELECT retry_count FROM pipeline_items WHERE id = ?1")
|
||||
.bind(story_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
count, 3,
|
||||
"retry_count must be 3 after three bump_retry_count calls"
|
||||
);
|
||||
}
|
||||
|
||||
/// Story 1087, AC2: the split-stage migration projects every supported
|
||||
/// wire-form `stage` string into the canonical `(pipeline, status)` pair.
|
||||
/// The fixture covers each Stage variant (and the legacy numeric-prefix
|
||||
|
||||
Reference in New Issue
Block a user