mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-09-02 09:39:37 +00:00
fix: Non-transactional multi-step context updates between metadata/to… (#1161)
* fix: Non-transactional multi-step context updates between metadata/token setup and DB * fix: code style
This commit is contained in:
@@ -87,6 +87,28 @@ impl ContextManager {
|
||||
Ok(f(context))
|
||||
}
|
||||
|
||||
/// Atomically update a job context and return the updated context.
|
||||
///
|
||||
/// This method holds the write lock for the entire update-and-read sequence,
|
||||
/// preventing concurrent workers from interleaving modifications between the
|
||||
/// update and the subsequent read (Issue #807: non-transactional context updates).
|
||||
/// Use this when you need to update context and immediately persist it to DB.
|
||||
pub async fn update_context_and_get<F>(
|
||||
&self,
|
||||
job_id: Uuid,
|
||||
f: F,
|
||||
) -> Result<JobContext, JobError>
|
||||
where
|
||||
F: FnOnce(&mut JobContext),
|
||||
{
|
||||
let mut contexts = self.contexts.write().await;
|
||||
let context = contexts
|
||||
.get_mut(&job_id)
|
||||
.ok_or(JobError::NotFound { id: job_id })?;
|
||||
f(context);
|
||||
Ok(context.clone())
|
||||
}
|
||||
|
||||
/// Get job memory.
|
||||
pub async fn get_memory(&self, job_id: Uuid) -> Result<Memory, JobError> {
|
||||
self.memories
|
||||
@@ -877,4 +899,70 @@ mod tests {
|
||||
|
||||
assert_eq!(manager.all_jobs().await.len(), 10);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_context_and_get_atomicity_regression_issue_807() {
|
||||
// Regression test for Issue #807: non-transactional context updates.
|
||||
// Verify that update_context_and_get returns the exact state that was set,
|
||||
// without allowing concurrent workers to interleave modifications.
|
||||
let manager = std::sync::Arc::new(ContextManager::new(100));
|
||||
let job_id = manager
|
||||
.create_job("Atomicity Test", "verify no race condition")
|
||||
.await
|
||||
.unwrap(); // safety: test code
|
||||
|
||||
// Update and get atomically, setting metadata
|
||||
let metadata = serde_json::json!({ "priority": "high", "user_id": 42 });
|
||||
let returned_ctx = manager
|
||||
.update_context_and_get(job_id, |ctx| {
|
||||
ctx.metadata = metadata.clone();
|
||||
ctx.max_tokens = 5000;
|
||||
})
|
||||
.await
|
||||
.unwrap(); // safety: test code
|
||||
|
||||
// Verify the returned context has the exact updates we set
|
||||
assert_eq!(returned_ctx.metadata, metadata); // safety: test code
|
||||
assert_eq!(returned_ctx.max_tokens, 5000); // safety: test code
|
||||
|
||||
// Verify a fresh get returns the same state
|
||||
let fresh_ctx = manager.get_context(job_id).await.unwrap(); // safety: test code
|
||||
assert_eq!(fresh_ctx.metadata, metadata); // safety: test code
|
||||
assert_eq!(fresh_ctx.max_tokens, 5000); // safety: test code
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_context_and_get_no_concurrent_interleave() {
|
||||
// Verify that concurrent updates cannot interleave during update_context_and_get.
|
||||
// If the lock were released too early, a concurrent state transition could
|
||||
// get mixed into the returned context.
|
||||
let manager = std::sync::Arc::new(ContextManager::new(100));
|
||||
let job_id = manager
|
||||
.create_job("Concurrent Race Test", "ensure atomicity")
|
||||
.await
|
||||
.unwrap(); // safety: test code
|
||||
|
||||
let metadata = serde_json::json!({ "test": "race_condition" });
|
||||
let metadata_clone = metadata.clone();
|
||||
|
||||
// Spawn a task that will update_context_and_get
|
||||
let mgr1 = std::sync::Arc::clone(&manager);
|
||||
let returned_ctx_handle = tokio::spawn(async move {
|
||||
mgr1.update_context_and_get(job_id, |ctx| {
|
||||
ctx.metadata = metadata_clone;
|
||||
ctx.max_tokens = 3000;
|
||||
})
|
||||
.await
|
||||
});
|
||||
|
||||
// The returned context should have *only* the metadata update, not any
|
||||
// concurrent state transitions that might happen during the operation.
|
||||
let returned_ctx = returned_ctx_handle.await.unwrap().unwrap(); // safety: test code
|
||||
|
||||
// Verify atomicity: returned context has the metadata we set
|
||||
assert_eq!(returned_ctx.metadata, metadata); // safety: test code
|
||||
assert_eq!(returned_ctx.max_tokens, 3000); // safety: test code
|
||||
// And it's in the initial state (Pending), not modified by concurrent workers
|
||||
assert_eq!(returned_ctx.state, crate::context::JobState::Pending); // safety: test code
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user