Swap: lock journals to one session; record the base file in the header

- open_ctx takes a non-blocking flock on the journal. A journal locked
  by another kte process is left alone: this session reports it once
  and does not write (records from two sessions used to interleave in
  one file). Opening a file whose journal is live in another session
  skips recovery instead of replaying or removing that journal.
- The header records the size and mtime of the file the journal's
  records apply to (flags bit 0, bytes 24..39; previously reserved, so
  no version change). Before offering recovery, a journal whose base no
  longer matches the file on disk (changed after a crash, or the crash
  fell between a save's rename and the journal reset) is treated as
  stale and offered for deletion, instead of replaying its edits onto
  different text. Journals from older kte (flag clear) replay as before.

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 7e16680a26
commit cb9b6bdf45
5 changed files with 202 additions and 9 deletions
+19 -1
View File
@@ -123,6 +123,16 @@ public:
// Returns empty string if filename is empty.
static std::string ComputeSwapPathForFilename(const std::string &filename);
// True if another process currently holds the journal at swap_path (it
// is live, not left by a crash): it must be neither replayed nor removed.
static bool JournalInUse(const std::string &swap_path);
// False if the journal records which version of `file_path` it applies
// to and the file on disk no longer matches (edited or replaced since
// the journal started); replaying it would apply edits to the wrong text.
// Journals without that record (older kte) are assumed to match.
static bool JournalMatchesFile(const std::string &swap_path, const std::string &file_path);
// Test-only hook to keep swap path logic centralized.
// (Avoid duplicating naming rules in unit tests.)
#ifdef KTE_TESTS
@@ -204,6 +214,14 @@ private:
bool gap{false};
std::uint64_t gap_chkpt_request_ns{0};
bool gap_unfixable_reported{false}; // gap on a buffer too large to checkpoint
// Another kte process holds this journal's lock: do not write to it.
bool locked_out{false};
// Identity of the file the journal's records apply to (captured when
// the journal starts) and written into its header, so recovery can
// tell whether the file changed since.
bool has_base{false};
std::uint64_t base_size{0};
std::int64_t base_mtime_ns{0};
};
struct Pending {
@@ -225,7 +243,7 @@ private:
static std::string SwapDirRoot();
static bool write_header(int fd);
static bool write_header(int fd, const JournalCtx &ctx);
static bool open_ctx(JournalCtx &ctx, const std::string &path, std::string &err);