feat: mem v2 - PR6 (consolidation) (#11374)

This commit is contained in:
jif-oai authored and GitHub committed 2026-02-11 00:02:57 +00:00
1 parent 2c9be54c9a
commit 674799d356
7 files changed
+910 -156

No files matched your search

+184
View File
@@ -885,6 +885,7 @@ mod tests {
use sqlx::Row;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use uuid::Uuid;
@@ -1125,6 +1126,133 @@ mod tests {
let _ = tokio::fs::remove_dir_all(codex_home).await;
}
#[tokio::test]
async fn stage1_concurrent_claim_for_same_thread_is_conflict_safe() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None)
.await
.expect("initialize runtime");
let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
runtime
.upsert_thread(&test_thread_metadata(
&codex_home,
thread_id,
codex_home.join("workspace"),
))
.await
.expect("upsert thread");
let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
let thread_id_a = thread_id;
let thread_id_b = thread_id;
let runtime_a = Arc::clone(&runtime);
let runtime_b = Arc::clone(&runtime);
let claim_with_retry = |runtime: Arc<StateRuntime>,
thread_id: ThreadId,
owner: ThreadId| async move {
for attempt in 0..5 {
match runtime
.try_claim_stage1_job(thread_id, owner, 100, 3_600, 64)
.await
{
Ok(outcome) => return outcome,
Err(err) if err.to_string().contains("database is locked") && attempt < 4 => {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
Err(err) => panic!("claim stage1 should not fail: {err}"),
}
}
panic!("claim stage1 should have returned within retry budget")
};
let (claim_a, claim_b) = tokio::join!(
claim_with_retry(runtime_a, thread_id_a, owner_a),
claim_with_retry(runtime_b, thread_id_b, owner_b),
);
let claim_outcomes = vec![claim_a, claim_b];
let claimed_count = claim_outcomes
.iter()
.filter(|outcome| matches!(outcome, Stage1JobClaimOutcome::Claimed { .. }))
.count();
assert_eq!(claimed_count, 1);
assert!(
claim_outcomes.iter().all(|outcome| {
matches!(
outcome,
Stage1JobClaimOutcome::Claimed { .. } | Stage1JobClaimOutcome::SkippedRunning
)
}),
"unexpected claim outcomes: {claim_outcomes:?}"
);
let _ = tokio::fs::remove_dir_all(codex_home).await;
}
#[tokio::test]
async fn stage1_concurrent_claims_respect_running_cap() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None)
.await
.expect("initialize runtime");
let thread_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
let thread_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
runtime
.upsert_thread(&test_thread_metadata(
&codex_home,
thread_a,
codex_home.join("workspace-a"),
))
.await
.expect("upsert thread a");
runtime
.upsert_thread(&test_thread_metadata(
&codex_home,
thread_b,
codex_home.join("workspace-b"),
))
.await
.expect("upsert thread b");
let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
let runtime_a = Arc::clone(&runtime);
let runtime_b = Arc::clone(&runtime);
let (claim_a, claim_b) = tokio::join!(
async move {
runtime_a
.try_claim_stage1_job(thread_a, owner_a, 100, 3_600, 1)
.await
.expect("claim stage1 thread a")
},
async move {
runtime_b
.try_claim_stage1_job(thread_b, owner_b, 101, 3_600, 1)
.await
.expect("claim stage1 thread b")
},
);
let claim_outcomes = vec![claim_a, claim_b];
let claimed_count = claim_outcomes
.iter()
.filter(|outcome| matches!(outcome, Stage1JobClaimOutcome::Claimed { .. }))
.count();
assert_eq!(claimed_count, 1);
assert!(
claim_outcomes
.iter()
.any(|outcome| { matches!(outcome, Stage1JobClaimOutcome::SkippedRunning) }),
"one concurrent claim should be throttled by running cap: {claim_outcomes:?}"
);
let _ = tokio::fs::remove_dir_all(codex_home).await;
}
#[tokio::test]
async fn claim_stage1_jobs_filters_by_age_idle_and_current_thread() {
let codex_home = unique_temp_dir();
@@ -1717,6 +1845,62 @@ WHERE kind = 'memory_stage1'
let _ = tokio::fs::remove_dir_all(codex_home).await;
}
#[tokio::test]
async fn phase2_failure_fallback_updates_unowned_running_job() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None)
.await
.expect("initialize runtime");
runtime
.enqueue_global_consolidation(400)
.await
.expect("enqueue global consolidation");
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner");
let claim = runtime
.try_claim_global_phase2_job(owner, 3_600)
.await
.expect("claim global consolidation");
let ownership_token = match claim {
Phase2JobClaimOutcome::Claimed {
ownership_token, ..
} => ownership_token,
other => panic!("unexpected claim outcome: {other:?}"),
};
sqlx::query("UPDATE jobs SET ownership_token = NULL WHERE kind = ? AND job_key = ?")
.bind("memory_consolidate_global")
.bind("global")
.execute(runtime.pool.as_ref())
.await
.expect("clear ownership token");
assert_eq!(
runtime
.mark_global_phase2_job_failed(ownership_token.as_str(), "lost", 3_600)
.await
.expect("mark phase2 failed with strict ownership"),
false,
"strict failure update should not match unowned running job"
);
assert!(
runtime
.mark_global_phase2_job_failed_if_unowned(ownership_token.as_str(), "lost", 3_600)
.await
.expect("fallback failure update should match unowned running job"),
"fallback failure update should transition the unowned running job"
);
let claim = runtime
.try_claim_global_phase2_job(ThreadId::new(), 3_600)
.await
.expect("claim after fallback failure");
assert_eq!(claim, Phase2JobClaimOutcome::SkippedNotDirty);
let _ = tokio::fs::remove_dir_all(codex_home).await;
}
fn test_thread_metadata(
codex_home: &Path,
thread_id: ThreadId,