store: set aside a torn last journal line on open; server waits for lock
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
A crash or power loss mid-append can leave a partial last line. The writer acknowledges an append only after its whole line is synced, so that line was never acknowledged: opening now saves those bytes to journal.jsonl.torn.<time>, truncates to the last newline, and journals journal.tail_repaired (replay and projection ignore it; the GUI shows it; books.digest reports it in journal_repairs). Damage before the last newline is still corruption and still refuses to open. Tested at every byte of the last append. The MCP server now waits up to ten seconds for the writer lock: a restarted server (a client reconnecting) raced the exiting one and died with Locked, which also made mcp_schedules_and_recurring_over_stdio flaky. The test harness now waits for each server to exit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
1 parent
22d1a3d449
commit
4af2f6c0bb
9 files changed
+293
-22
No files matched your search
@@ -9,10 +9,18 @@ struct Mcp {
|
||||
stdin: ChildStdin,
|
||||
stdout: BufReader<std::process::ChildStdout>,
|
||||
next_id: u64,
|
||||
#[allow(dead_code)]
|
||||
child: Child,
|
||||
}
|
||||
|
||||
/// Closing stdin ends the server; wait for it to exit, so the next server
|
||||
/// on the same books never races it for the writer lock.
|
||||
impl Drop for Mcp {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.child.kill();
|
||||
let _ = self.child.wait();
|
||||
}
|
||||
}
|
||||
|
||||
impl Mcp {
|
||||
fn spawn(data_dir: &std::path::Path) -> Self {
|
||||
Self::spawn_with(data_dir, &[])
|
||||
@@ -1673,3 +1681,29 @@ fn mcp_receipts_always_required_when_threshold_is_zero() {
|
||||
"a $5 entry needs a receipt too: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A client reconnecting starts a new server while the old one still has
|
||||
/// the books: the new one waits for the lock instead of dying.
|
||||
#[test]
|
||||
fn a_restarted_server_waits_for_the_old_one_to_let_go() {
|
||||
let dir = test_dir("restart-race");
|
||||
let init = |mcp: &mut Mcp| {
|
||||
mcp.request(
|
||||
"initialize",
|
||||
json!({
|
||||
"protocolVersion": "2025-06-18",
|
||||
"capabilities": {},
|
||||
"clientInfo": {"name": "smoke-test", "version": "0.1.0"},
|
||||
}),
|
||||
);
|
||||
mcp.notify("notifications/initialized");
|
||||
};
|
||||
let mut old = Mcp::spawn(&dir);
|
||||
init(&mut old);
|
||||
let mut new = Mcp::spawn(&dir);
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
drop(old);
|
||||
init(&mut new);
|
||||
let chart = new.call("books.chart", json!({}));
|
||||
assert!(chart.is_object());
|
||||
}
|
||||
Reference in new issue
Block a user