mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 17:09:13 -10:00
fix(session): preserve checkpoints when reset saves fail [skip ci]
Queue a constructed reset snapshot as one save-then-clear commit, and make coalesced snapshot/clear requests use the same conditional clear. This preserves the recovery checkpoint if the document write fails and avoids a race between separate requests.
Existing save-failure case first failed with the checkpoint removed; 17 persistence, backlog and session lifecycle cases passed on frozen b42759ee9b plus these files, with 2,759 inputs unchanged. Snapshot-construction failure still follows the prior fallback. Linker unwind warning was nonfatal.
This commit is contained in:
1 parent
b42759ee9b
commit
c4a4f42e9c
2 files changed
+34
-19
No files matched your search
@@ -650,22 +650,22 @@ impl PendingState {
|
||||
// session only — other sessions' pending work is untouched.
|
||||
// An explicit clear is also a user-owned boundary (e.g.
|
||||
// `/new`): it supersedes a pending completion while keeping
|
||||
// a newer explicit snapshot for the same session.
|
||||
// a newer explicit snapshot and clearing only after its save.
|
||||
self.checkpoints.remove(&session_id);
|
||||
if let Some(PendingSession::Snapshot(session)) =
|
||||
self.completed_commits.remove(&session_id)
|
||||
{
|
||||
self.sessions
|
||||
.insert(session_id.clone(), PendingSession::Snapshot(session));
|
||||
let completed = self.completed_commits.remove(&session_id);
|
||||
let session = self.sessions.remove(&session_id);
|
||||
let snapshot = match (session, completed) {
|
||||
(Some(PendingSession::Snapshot(session)), _)
|
||||
| (_, Some(PendingSession::Snapshot(session))) => Some(session),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(snapshot) = snapshot {
|
||||
self.checkpoint_clears.remove(&session_id);
|
||||
self.completed_commits
|
||||
.insert(session_id, PendingSession::Snapshot(snapshot));
|
||||
} else {
|
||||
self.checkpoint_clears.insert(session_id);
|
||||
}
|
||||
if self
|
||||
.sessions
|
||||
.get(&session_id)
|
||||
.is_some_and(|session| matches!(session, PendingSession::Completion(_)))
|
||||
{
|
||||
self.sessions.remove(&session_id);
|
||||
}
|
||||
self.checkpoint_clears.insert(session_id);
|
||||
}
|
||||
PersistRequest::FlushAndReport { reply } => return Control::Flush(reply),
|
||||
PersistRequest::Shutdown => return Control::Shutdown,
|
||||
@@ -1343,7 +1343,9 @@ mod tests {
|
||||
block_session_file(&sessions_dir, &session_id);
|
||||
let (handle, task) = spawn_persistence_actor(manager);
|
||||
|
||||
handle.try_send(PersistRequest::CompletedCommit { session });
|
||||
handle.try_send(PersistRequest::CompletedCommit {
|
||||
session: session.clone(),
|
||||
});
|
||||
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
|
||||
handle.try_send(PersistRequest::FlushAndReport { reply: reply_tx });
|
||||
let report = reply_rx.await.expect("flush report reply");
|
||||
@@ -1367,6 +1369,18 @@ mod tests {
|
||||
checkpoint_path.exists(),
|
||||
"a failed session save must retain the crash-recovery checkpoint"
|
||||
);
|
||||
handle.try_send(PersistRequest::SessionSnapshot(session));
|
||||
handle.try_send(PersistRequest::ClearCheckpoint {
|
||||
session_id: session_id.clone(),
|
||||
});
|
||||
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
|
||||
handle.try_send(PersistRequest::FlushAndReport { reply: reply_tx });
|
||||
let report = reply_rx.await.expect("reset flush report reply");
|
||||
assert!(!report.failures.is_empty());
|
||||
assert!(
|
||||
checkpoint_path.exists(),
|
||||
"a failed reset save must retain the crash-recovery checkpoint"
|
||||
);
|
||||
handle.try_send(PersistRequest::Shutdown);
|
||||
task.await.expect("persistence actor join");
|
||||
}
|
||||
@@ -1581,7 +1595,7 @@ mod tests {
|
||||
session_id: session_id.clone(),
|
||||
});
|
||||
assert!(matches!(
|
||||
&pending.sessions[&session_id],
|
||||
&pending.completed_commits[&session_id],
|
||||
PendingSession::Snapshot(session) if session.metadata.title == "Reset session"
|
||||
));
|
||||
let saved = verification_manager.load_session(&session_id).unwrap();
|
||||
@@ -1606,7 +1620,7 @@ mod tests {
|
||||
session_id: session_id.clone(),
|
||||
});
|
||||
assert!(matches!(
|
||||
&pending.sessions[&session_id],
|
||||
&pending.completed_commits[&session_id],
|
||||
PendingSession::Snapshot(session) if session.metadata.title == "Newer reset"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -487,19 +487,20 @@ pub(crate) fn persist_full_reset_snapshot(app: &mut App) {
|
||||
app.current_session_id = Some(session.metadata.id.clone());
|
||||
if let Err(err) = persist_with_pending_work_boundary(
|
||||
app,
|
||||
PersistRequest::SessionSnapshot(session),
|
||||
PersistRequest::CompletedCommit { session },
|
||||
&projection,
|
||||
) {
|
||||
app.status_message = Some(format!(
|
||||
"To-do list update pending while saving reset snapshot ({err})"
|
||||
));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// `/clear` and `/new` are explicit boundaries. Never let an older
|
||||
// in-flight checkpoint resurrect the session the user just discarded,
|
||||
// even if the replacement snapshot could not be constructed.
|
||||
// `build_session_snapshot` reuses `current_session_id`, so this id is the
|
||||
// discarded session's id whether or not the snapshot above succeeded.
|
||||
// discarded session's id when the replacement snapshot could not be built.
|
||||
if let Some(session_id) = app.current_session_id.clone() {
|
||||
persistence_actor::persist(PersistRequest::ClearCheckpoint { session_id });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user