store: set aside a torn last journal line on open; server waits for lock
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
55f9e8f6e0
commit
54f632dfea
9 files changed
+293
-22
No files matched your search
@@ -116,7 +116,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let company = settings.company;
|
||||
|
||||
let mut store = if data_dir.join(ledger_store::JOURNAL_FILE).exists() {
|
||||
Store::open(&data_dir)?
|
||||
open_waiting_for_lock(&data_dir)?
|
||||
} else {
|
||||
let code = config.base_currency.as_deref().unwrap_or("USD");
|
||||
let currency = Currency::new(code).map_err(|_| format!("bad base currency {code:?}"))?;
|
||||
@@ -180,3 +180,20 @@ async fn serve_http(
|
||||
axum::serve(listener, router).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Open the books, waiting up to ten seconds for another writer to let go.
|
||||
/// A restarted server (a client reconnecting) often starts while the old
|
||||
/// process is still exiting with the lock; waiting turns that race into a
|
||||
/// short delay. A writer that keeps the books longer is a real conflict,
|
||||
/// and the refusal says so.
|
||||
fn open_waiting_for_lock(data_dir: &std::path::Path) -> Result<Store, ledger_store::StoreError> {
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
|
||||
loop {
|
||||
match Store::open(data_dir) {
|
||||
Err(ledger_store::StoreError::Locked(_)) if std::time::Instant::now() < deadline => {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
other => return other,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1623,11 +1623,23 @@ impl BooksServer {
|
||||
why: r.review_why.to_string(),
|
||||
})
|
||||
.collect();
|
||||
let journal_repairs = store
|
||||
.tail_repairs()
|
||||
.map_err(|e| e.to_string())?
|
||||
.into_iter()
|
||||
.filter(|(at, _, _)| at.get(..10).is_some_and(|d| d >= p.since.as_str()))
|
||||
.map(|(at, bytes, saved_as)| JournalRepair {
|
||||
at,
|
||||
bytes,
|
||||
saved_as,
|
||||
})
|
||||
.collect();
|
||||
Ok(Json(DigestOut {
|
||||
since: since.to_string(),
|
||||
entries,
|
||||
totals_by_account: totals,
|
||||
stale_rules,
|
||||
journal_repairs,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -1266,6 +1266,22 @@ pub struct DigestOut {
|
||||
/// for everyone with a pull request (or at least an issue) at
|
||||
/// <https://github.com/swaits/wanderbooks>.
|
||||
pub stale_rules: Vec<StaleRule>,
|
||||
/// Appends cut off mid-line (a crash or power loss) that were set aside
|
||||
/// when the books were opened, since `since`. Nothing acknowledged was
|
||||
/// lost; tell the human, and if they had just asked for a change,
|
||||
/// check it is there and redo it if not.
|
||||
pub journal_repairs: Vec<JournalRepair>,
|
||||
}
|
||||
|
||||
/// A torn journal tail set aside on open.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct JournalRepair {
|
||||
/// When it was set aside (UTC timestamp).
|
||||
pub at: String,
|
||||
/// How many bytes were cut off.
|
||||
pub bytes: u64,
|
||||
/// The file beside the journal they were saved to.
|
||||
pub saved_as: String,
|
||||
}
|
||||
|
||||
/// A dated rule due for review (see `ledger_core::rules`).
|
||||
|
||||
Reference in new issue
Block a user