Swap: chunked checkpoints for large buffers; scaled thresholds; single replay

- A checkpoint larger than one record (16 MiB) is written as CHKPT_BEGIN
  [size u64, CRC-32], CHKPT_DATA chunks and CHKPT_END. Replay assembles it
  and applies it only when complete with the size and CRC matching; an
  unfinished one is a torn tail (valid_bytes stops before it). Buffers
  over 16 MiB could not be checkpointed before: a lost record left their
  journal behind for good, and they were never compacted. Records are
  written as segments straight from the snapshot. Header version 2;
  version 1 journals are still read.
- Checkpoint thresholds scale with the buffer size (edits of half its
  size; the timer only after a sixteenth; compaction at twice its size;
  gap retries spaced by size), and a checkpoint that would compact is
  written straight into the compacted journal instead of being appended
  and then copied.
- The recovery prompt keeps the content it replayed for its preview and
  installs it on 'y', instead of replaying the journal a second time.
- A base file over 64 MiB gets its CRC from the writer thread before it
  creates the journal (it had none, so touching such a file made its
  journal unusable for recovery).
- kte_tests takes optional name substrings to run a subset of tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
This commit is contained in:
Claude
2026-09-24 09:16:04 -07:00
committed by Kyle Isom
parent fc15a58a2b
commit a33c1759e7
9 changed files with 491 additions and 96 deletions
+22 -7
View File
@@ -395,6 +395,9 @@ Editor::CancelRecoveryPrompt()
pending_recovery_open_ = PendingOpen{};
pending_recovery_swap_path_.clear();
pending_recovery_replay_err_.clear();
std::string().swap(pending_recovery_content_);
pending_recovery_valid_bytes_ = 0;
pending_recovery_have_content_ = false;
}
@@ -407,6 +410,9 @@ Editor::ResolveRecoveryPrompt(const bool yes)
const PendingOpen req = pending_recovery_open_;
const std::string swp = pending_recovery_swap_path_;
const std::string rerr_s = pending_recovery_replay_err_;
std::string content = std::move(pending_recovery_content_);
const bool have_content = pending_recovery_have_content_;
std::uint64_t valid_bytes = pending_recovery_valid_bytes_;
CancelRecoveryPrompt();
std::string err;
@@ -421,11 +427,16 @@ Editor::ResolveRecoveryPrompt(const bool yes)
SetStatus("Recovery failed: no buffer");
return false;
}
std::string rerr;
std::uint64_t valid_bytes = 0;
if (!kte::SwapManager::ReplayFile(*b, swp, rerr, &valid_bytes)) {
SetStatus("Swap recovery failed: " + rerr);
return false;
if (have_content) {
// The prompt already replayed the journal; install that.
b->replace_all_bytes(content);
std::string().swap(content);
} else {
std::string rerr;
if (!kte::SwapManager::ReplayFile(*b, swp, rerr, &valid_bytes)) {
SetStatus("Swap recovery failed: " + rerr);
return false;
}
}
// This session keeps appending to the same journal. Drop a torn
// final record first, or new records would land behind it and the
@@ -581,8 +592,9 @@ Editor::process_pending_opens_()
if (tmp.OpenFromFile(req.path, oerr)) {
const std::string orig = buffer_bytes_via_views(tmp);
std::string rerr;
if (kte::SwapManager::ReplayFile(tmp, swp, rerr)) {
const std::string rec = buffer_bytes_via_views(tmp);
std::uint64_t valid = 0;
if (kte::SwapManager::ReplayFile(tmp, swp, rerr, &valid)) {
std::string rec = buffer_bytes_via_views(tmp);
if (rec == orig) {
// Nothing to recover. Remove the journal rather than
// appending this session's records to it (it may end in a
@@ -592,6 +604,9 @@ Editor::process_pending_opens_()
pending_recovery_prompt_ = RecoveryPromptKind::RecoverOrDiscard;
pending_recovery_open_ = req;
pending_recovery_swap_path_ = swp;
pending_recovery_content_ = std::move(rec);
pending_recovery_valid_bytes_ = valid;
pending_recovery_have_content_ = true;
StartPrompt(PromptKind::Confirm, "Recover", "");
SetStatus("Recover swap edits for " + req.path + "? (y/n, C-g cancel)");
return opened_any;