mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
feat: make rollout recorder reliable against errors (#17214)
The rollout writer now keeps an owned/monitored task handle, returns real Result acks for flush/persist/shutdown, retries failed flushes by reopening the rollout file, and keeps buffered items until they are successfully written. Session flushes are now real durability barriers for fork/rollback/read-after-write paths, while turn completion surfaces a warning if the rollout still cannot be saved after recovery.
This commit is contained in:
1 parent
085ffb4456
commit
8035cb03f1
10 files changed
+536
-191
No files matched your search
@@ -63,7 +63,7 @@ fn write_session_file(root: &Path, ts: &str, uuid: Uuid) -> std::io::Result<Path
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn recorder_materializes_only_after_explicit_persist() -> std::io::Result<()> {
|
||||
async fn recorder_materializes_on_flush_with_pending_items() -> std::io::Result<()> {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let thread_id = ThreadId::new();
|
||||
@@ -85,7 +85,7 @@ async fn recorder_materializes_only_after_explicit_persist() -> std::io::Result<
|
||||
let rollout_path = recorder.rollout_path().to_path_buf();
|
||||
assert!(
|
||||
!rollout_path.exists(),
|
||||
"rollout file should not exist before first user message"
|
||||
"rollout file should not exist before the first recordable item"
|
||||
);
|
||||
|
||||
recorder
|
||||
@@ -99,8 +99,8 @@ async fn recorder_materializes_only_after_explicit_persist() -> std::io::Result<
|
||||
.await?;
|
||||
recorder.flush().await?;
|
||||
assert!(
|
||||
!rollout_path.exists(),
|
||||
"rollout file should remain deferred before first user message"
|
||||
rollout_path.exists(),
|
||||
"flush with pending items should materialize the rollout"
|
||||
);
|
||||
|
||||
recorder
|
||||
@@ -114,10 +114,6 @@ async fn recorder_materializes_only_after_explicit_persist() -> std::io::Result<
|
||||
))])
|
||||
.await?;
|
||||
recorder.flush().await?;
|
||||
assert!(
|
||||
!rollout_path.exists(),
|
||||
"user-message-like items should not materialize without explicit persist"
|
||||
);
|
||||
|
||||
recorder.persist().await?;
|
||||
// Second call verifies `persist()` is idempotent after materialization.
|
||||
@@ -146,6 +142,96 @@ async fn recorder_materializes_only_after_explicit_persist() -> std::io::Result<
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn persist_reports_filesystem_error_and_retries_buffered_items() -> std::io::Result<()> {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let thread_id = ThreadId::new();
|
||||
let recorder = RolloutRecorder::new(
|
||||
&config,
|
||||
RolloutRecorderParams::new(
|
||||
thread_id,
|
||||
/*forked_from_id*/ None,
|
||||
SessionSource::Exec,
|
||||
BaseInstructions::default(),
|
||||
Vec::new(),
|
||||
EventPersistenceMode::Limited,
|
||||
),
|
||||
/*state_db_ctx*/ None,
|
||||
/*state_builder*/ None,
|
||||
)
|
||||
.await?;
|
||||
let rollout_path = recorder.rollout_path().to_path_buf();
|
||||
|
||||
recorder
|
||||
.record_items(&[RolloutItem::EventMsg(EventMsg::AgentMessage(
|
||||
AgentMessageEvent {
|
||||
message: "buffered-before-persist".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
},
|
||||
))])
|
||||
.await?;
|
||||
let sessions_blocker_path = home.path().join("sessions");
|
||||
File::create(&sessions_blocker_path)?;
|
||||
|
||||
let err = recorder
|
||||
.persist()
|
||||
.await
|
||||
.expect_err("blocked sessions directory should fail persist");
|
||||
assert_ne!(err.kind(), std::io::ErrorKind::Interrupted);
|
||||
assert!(
|
||||
!rollout_path.exists(),
|
||||
"failed persist should keep the rollout deferred"
|
||||
);
|
||||
|
||||
fs::remove_file(sessions_blocker_path)?;
|
||||
recorder.flush().await?;
|
||||
let text = std::fs::read_to_string(&rollout_path)?;
|
||||
assert!(
|
||||
text.contains("buffered-before-persist"),
|
||||
"retry should preserve items buffered before the failed persist"
|
||||
);
|
||||
|
||||
recorder.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn writer_state_retries_write_error_before_reporting_flush_success() -> std::io::Result<()> {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let rollout_path = home.path().join("rollout.jsonl");
|
||||
File::create(&rollout_path)?;
|
||||
let read_only_file = std::fs::OpenOptions::new().read(true).open(&rollout_path)?;
|
||||
let mut state = RolloutWriterState::new(
|
||||
Some(tokio::fs::File::from_std(read_only_file)),
|
||||
/*deferred_log_file_info*/ None,
|
||||
/*meta*/ None,
|
||||
home.path().to_path_buf(),
|
||||
rollout_path.clone(),
|
||||
/*state_db_ctx*/ None,
|
||||
/*state_builder*/ None,
|
||||
config.model_provider_id.clone(),
|
||||
config.generate_memories,
|
||||
);
|
||||
state.add_items(vec![RolloutItem::EventMsg(EventMsg::AgentMessage(
|
||||
AgentMessageEvent {
|
||||
message: "queued-after-writer-error".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
},
|
||||
))]);
|
||||
|
||||
state.flush().await?;
|
||||
let text_after_retry = std::fs::read_to_string(&rollout_path)?;
|
||||
assert!(
|
||||
text_after_retry.contains("queued-after-writer-error"),
|
||||
"flush should retry after reopening and write buffered items"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn metadata_irrelevant_events_touch_state_db_updated_at() -> std::io::Result<()> {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
|
||||
Reference in new issue
Block a user