Files
kte/Swap.cc
T
Claude 061e836fa4 Swap/recovery: second-pass fixes from the follow-up review
- Recovery works when another named buffer is current. On that path
  OpenFile's NotifyFilenameChanged deleted the journal (old path ==
  new path) that was about to be replayed; the swap was lost and the
  file opened without the edits (pre-existing).
- After recovering from a journal with a torn final record, the torn
  bytes are truncated before the session appends to it (ReplayFile now
  reports the valid prefix length). A journal that recovers nothing is
  removed instead of appended to.
- Reload restarts the journal; recovery used to replay the edits the
  reload discarded.
- Opening a file that is already open switches to its buffer (in
  OpenFile and before the recovery check). Two buffers for one file
  interleaved records in one journal, and closing either unlinked it.
- Swap file names escape '!' and '%', so distinct paths no longer
  share a journal ('/x/a!b' vs '/x/a/b'). Swap files for paths that
  contain those characters get new names.
- The gap flag is cleared when the journal restarts, and a gap that
  cannot be closed (buffer too large to checkpoint) is reported once.
- crc32's table is initialised thread-safely; writer-thread error
  reports identify the buffer by journal path instead of racing on
  Buffer::Filename(); Close() no longer retries on EINTR (the fd is
  already released and may have been reused).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
2026-09-24 09:16:04 -07:00

1683 lines
44 KiB
C++

#include "Swap.h"
#include "Buffer.h"
#include "ErrorHandler.h"
#include "SyscallWrappers.h"
#include "ErrorRecovery.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <cstdlib>
#include <fstream>
#include <filesystem>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <cerrno>
namespace fs = std::filesystem;
namespace kte {
namespace {
// Records store their payload length in 24 bits.
constexpr std::size_t kMaxRecordPayload = 0xFFFFFFu;
// While a journal has a gap, request a recovery checkpoint at most this often.
constexpr std::uint64_t kGapCheckpointIntervalNs = 1000000000ULL;
} // namespace
namespace {
constexpr std::uint8_t MAGIC[8] = {'K', 'T', 'E', '_', 'S', 'W', 'P', '\0'};
constexpr std::uint32_t VERSION = 1;
static std::string
snapshot_buffer_bytes(const Buffer &b)
{
const std::size_t nrows = b.Nrows();
std::string out;
// Cheap lower bound: sum of row sizes.
std::size_t approx = 0;
for (std::size_t i = 0; i < nrows; i++)
approx += b.GetLineView(i).size();
out.reserve(approx);
for (std::size_t i = 0; i < nrows; i++) {
auto v = b.GetLineView(i);
out.append(v.data(), v.size());
}
return out;
}
static fs::path
xdg_state_home()
{
if (const char *p = std::getenv("XDG_STATE_HOME")) {
if (*p)
return fs::path(p);
}
if (const char *home = std::getenv("HOME")) {
if (*home)
return fs::path(home) / ".local" / "state";
}
// Last resort: still provide a stable per-user-ish location.
return fs::temp_directory_path() / "kte" / "state";
}
static fs::path
swap_root_dir()
{
return xdg_state_home() / "kte" / "swap";
}
static std::uint64_t
fnv1a64(std::string_view s)
{
std::uint64_t h = 14695981039346656037ULL;
for (unsigned char ch: s) {
h ^= (std::uint64_t) ch;
h *= 1099511628211ULL;
}
return h;
}
static std::string
hex_u64(std::uint64_t v)
{
static const char *kHex = "0123456789abcdef";
char out[16];
for (int i = 15; i >= 0; --i) {
out[i] = kHex[v & 0xFULL];
v >>= 4;
}
return std::string(out, sizeof(out));
}
// Write all bytes in buf to fd, handling EINTR and partial writes.
static bool
write_full(int fd, const void *buf, size_t len)
{
const std::uint8_t *p = static_cast<const std::uint8_t *>(buf);
while (len > 0) {
ssize_t n = ::write(fd, p, len);
if (n < 0) {
if (errno == EINTR)
continue;
return false;
}
if (n == 0)
return false; // shouldn't happen for regular files; treat as error
p += static_cast<size_t>(n);
len -= static_cast<size_t>(n);
}
return true;
}
static std::string
encode_path_key(std::string s)
{
// Turn an absolute path like "/home/kyle/tmp/test.txt" into
// "home!kyle!tmp!test.txt" so swap files are human-identifiable.
//
// Notes:
// - We strip a single leading path separator so absolute paths don't start with '!'.
// - We replace both '/' and '\\' with '!'.
// - We leave other characters as-is (spaces are OK on POSIX).
// - '!' and '%' in the path itself are escaped as %21 and %25, so the
// mapping is one-to-one: "/x/a!b" and "/x/a/b" used to share a journal.
if (!s.empty() && (s[0] == '/' || s[0] == '\\'))
s.erase(0, 1);
std::string out;
out.reserve(s.size());
for (char ch: s) {
if (ch == '/' || ch == '\\')
out.push_back('!');
else if (ch == '!')
out += "%21";
else if (ch == '%')
out += "%25";
else
out.push_back(ch);
}
return out;
}
static std::string
compute_swap_path_for_filename(const std::string &filename)
{
if (filename.empty())
return std::string();
// Always place swap under an XDG home-appropriate state directory.
// This avoids cluttering working directories and prevents stomping on
// swap files when multiple different paths share the same basename.
fs::path root = swap_root_dir();
fs::path p(filename);
std::string key;
try {
key = fs::weakly_canonical(p).string();
} catch (...) {
try {
key = fs::absolute(p).string();
} catch (...) {
key = filename;
}
}
std::string encoded = encode_path_key(key);
if (!encoded.empty()) {
std::string name = encoded + ".swp";
// Avoid filesystem/path length issues; fall back to hashed naming.
// NAME_MAX is often 255 on POSIX, but keep extra headroom.
if (name.size() <= 200) {
return (root / name).string();
}
}
// Fallback: stable, shorter name based on basename + hash.
std::string base = p.filename().string();
const std::string name = base + "." + hex_u64(fnv1a64(key)) + ".swp";
return (root / name).string();
}
}
SwapManager::SwapManager()
{
running_.store(true);
worker_ = std::thread([this] {
this->writer_loop();
});
// Best-effort prune of old swap files.
// Safe early in startup: journals_ is still empty and no fds are open yet.
if (cfg_.prune_on_startup) {
PruneSwapDir();
}
}
SwapManager::~SwapManager()
{
// Best-effort: drain queued records before stopping the writer.
Flush();
running_.store(false);
cv_.notify_all();
if (worker_.joinable())
worker_.join();
// Close all journals
for (auto &kv: journals_) {
close_ctx(kv.second);
}
}
void
SwapManager::Checkpoint(Buffer *buf)
{
if (buf) {
RecordCheckpoint(*buf, false);
return;
}
// All buffers
std::vector<Buffer *> bufs;
{
std::lock_guard<std::mutex> lg(mtx_);
bufs.reserve(journals_.size());
for (auto &kv: journals_) {
bufs.push_back(kv.first);
}
}
for (Buffer *b: bufs) {
if (b)
RecordCheckpoint(*b, false);
}
}
void
SwapManager::Flush(Buffer *buf)
{
(void) buf; // stage 1: flushes all buffers
std::unique_lock<std::mutex> lk(mtx_);
const std::uint64_t target = next_seq_;
// Wake the writer in case it's waiting on the interval.
cv_.notify_one();
cv_.wait(lk, [&] {
return queue_.empty() && inflight_ == 0 && last_processed_ >= target;
});
}
void
SwapManager::BufferRecorder::OnInsert(int row, int col, std::string_view bytes)
{
m_.RecordInsert(buf_, row, col, bytes);
}
void
SwapManager::BufferRecorder::OnDelete(int row, int col, std::size_t len)
{
m_.RecordDelete(buf_, row, col, len);
}
SwapRecorder *
SwapManager::RecorderFor(Buffer *buf)
{
if (!buf)
return nullptr;
std::lock_guard<std::mutex> lg(mtx_);
auto it = recorders_.find(buf);
if (it != recorders_.end())
return it->second.get();
// Create on-demand. Recording calls will no-op until Attach() has been called.
auto rec = std::make_unique<BufferRecorder>(*this, *buf);
SwapRecorder *ptr = rec.get();
recorders_[buf] = std::move(rec);
return ptr;
}
SwapRecorder *
SwapManager::Rehome(Buffer *old_addr, Buffer *new_addr)
{
if (!old_addr || !new_addr || old_addr == new_addr)
return nullptr;
std::lock_guard<std::mutex> lg(mtx_);
SwapRecorder *result = nullptr;
auto jit = journals_.find(old_addr);
if (jit != journals_.end()) {
JournalCtx ctx = std::move(jit->second);
journals_.erase(jit);
journals_[new_addr] = std::move(ctx);
}
auto rit = recorders_.find(old_addr);
if (rit != recorders_.end()) {
recorders_.erase(rit);
// BufferRecorder binds a Buffer& at construction, so it can't be
// repointed in place; rebuild it against the buffer's new address.
auto rec = std::make_unique<BufferRecorder>(*this, *new_addr);
result = rec.get();
recorders_[new_addr] = std::move(rec);
}
// Defensive: any record still queued (not yet drained by the writer thread)
// for the old address must follow the buffer to its new location. Callers
// are expected to Flush() before rehoming so this should normally be a no-op.
for (auto &p: queue_) {
if (p.buf == old_addr)
p.buf = new_addr;
}
return result;
}
void
SwapManager::Attach(Buffer *buf)
{
if (!buf)
return;
std::lock_guard<std::mutex> lg(mtx_);
JournalCtx &ctx = journals_[buf];
if (ctx.path.empty())
ctx.path = ComputeSidecarPath(*buf);
// Ensure a recorder exists as well.
if (recorders_.find(buf) == recorders_.end()) {
recorders_[buf] = std::make_unique<BufferRecorder>(*this, *buf);
}
}
void
SwapManager::Detach(Buffer *buf, const bool remove_file)
{
if (!buf) {
return;
}
// Write a best-effort final checkpoint before suspending and closing.
// If the caller requested removal, skip the final checkpoint so the file can be deleted.
if (!remove_file)
RecordCheckpoint(*buf, true);
{
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(buf);
if (it != journals_.end()) {
it->second.suspended = true;
}
}
Flush(buf);
std::string path;
{
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(buf);
if (it != journals_.end()) {
path = it->second.path;
close_ctx(it->second);
journals_.erase(it);
}
recorders_.erase(buf);
}
if (remove_file && !path.empty()) {
(void) std::remove(path.c_str());
}
}
void
SwapManager::ResetJournal(Buffer &buf)
{
std::string path;
{
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(&buf);
if (it == journals_.end())
return;
JournalCtx &ctx = it->second;
if (ctx.path.empty())
ctx.path = ComputeSidecarPath(buf);
path = ctx.path;
ctx.suspended = true;
}
Flush(&buf);
{
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(&buf);
if (it == journals_.end())
return;
JournalCtx &ctx = it->second;
close_ctx(ctx);
ctx.header_ok = false;
ctx.gap = false; // the journal restarts from the saved file
ctx.gap_chkpt_request_ns = 0;
ctx.gap_unfixable_reported = false;
ctx.last_flush_ns = 0;
ctx.last_fsync_ns = 0;
ctx.last_chkpt_ns = 0;
ctx.edit_bytes_since_chkpt = 0;
ctx.approx_size_bytes = 0;
ctx.suspended = false;
}
if (!path.empty()) {
(void) std::remove(path.c_str());
}
}
std::string
SwapManager::SwapDirRoot()
{
return swap_root_dir().string();
}
void
SwapManager::PruneSwapDir()
{
SwapConfig cfg;
std::vector<std::string> active;
{
std::lock_guard<std::mutex> lg(mtx_);
cfg = cfg_;
active.reserve(journals_.size());
for (const auto &kv: journals_) {
if (!kv.second.path.empty())
active.push_back(kv.second.path);
}
}
const fs::path root = swap_root_dir();
std::error_code ec;
if (!fs::exists(root, ec) || ec)
return;
struct Entry {
fs::path path;
std::filesystem::file_time_type mtime;
};
std::vector<Entry> swps;
for (auto it = fs::directory_iterator(root, ec); !ec && it != fs::directory_iterator(); it.increment(ec)) {
const fs::path p = it->path();
if (p.extension() != ".swp")
continue;
// Never delete active journals.
const std::string ps = p.string();
bool is_active = false;
for (const auto &a: active) {
if (a == ps) {
is_active = true;
break;
}
}
if (is_active)
continue;
std::error_code ec2;
if (!it->is_regular_file(ec2) || ec2)
continue;
auto tm = fs::last_write_time(p, ec2);
if (ec2)
continue;
swps.push_back({p, tm});
}
if (swps.empty())
return;
// Sort newest first.
std::sort(swps.begin(), swps.end(), [](const Entry &a, const Entry &b) {
return a.mtime > b.mtime;
});
// Convert age threshold.
auto now = std::filesystem::file_time_type::clock::now();
auto max_age = std::chrono::hours(24) * static_cast<long long>(cfg.prune_max_age_days);
std::size_t kept = 0;
for (const auto &e: swps) {
bool too_old = false;
if (cfg.prune_max_age_days > 0) {
// If file_time_type isn't system_clock, duration arithmetic still works.
if (now - e.mtime > max_age)
too_old = true;
}
bool over_limit = (cfg.prune_max_files > 0) && (kept >= cfg.prune_max_files);
if (too_old || over_limit) {
std::error_code ec3;
fs::remove(e.path, ec3);
} else {
++kept;
}
}
}
void
SwapManager::NotifyFilenameChanged(Buffer &buf)
{
// No checkpoint here: the old journal is deleted below, and every caller
// has just loaded or saved the file, so the new journal's base is the
// file's content.
std::string old_path;
{
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(&buf);
if (it == journals_.end())
return;
old_path = it->second.path;
it->second.suspended = true;
}
Flush(&buf);
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(&buf);
if (it == journals_.end())
return;
JournalCtx &ctx = it->second;
close_ctx(ctx);
const std::string new_path = ComputeSidecarPath(buf);
// When the path is unchanged (Attach already pointed the journal at this
// file's swap), that file may be the one the caller is about to replay
// for recovery: removing it here lost the recovery.
if (!old_path.empty() && old_path != new_path)
(void) std::remove(old_path.c_str());
ctx.gap = false;
ctx.gap_chkpt_request_ns = 0;
ctx.gap_unfixable_reported = false;
ctx.path = new_path;
ctx.suspended = false;
ctx.header_ok = false;
ctx.last_flush_ns = 0;
ctx.last_fsync_ns = 0;
ctx.last_chkpt_ns = 0;
ctx.edit_bytes_since_chkpt = 0;
ctx.approx_size_bytes = 0;
}
void
SwapManager::SetSuspended(Buffer &buf, bool on)
{
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(&buf);
if (it == journals_.end())
return;
it->second.suspended = on;
}
SwapManager::SuspendGuard::SuspendGuard(SwapManager &m, Buffer *b)
: m_(m), buf_(b), prev_(false)
{
if (!buf_)
return;
{
std::lock_guard<std::mutex> lg(m_.mtx_);
auto it = m_.journals_.find(buf_);
if (it != m_.journals_.end()) {
prev_ = it->second.suspended;
it->second.suspended = true;
}
}
}
SwapManager::SuspendGuard::~SuspendGuard()
{
if (!buf_)
return;
std::lock_guard<std::mutex> lg(m_.mtx_);
auto it = m_.journals_.find(buf_);
if (it != m_.journals_.end()) {
it->second.suspended = prev_;
}
}
std::string
SwapManager::ComputeSidecarPath(const Buffer &buf)
{
fs::path root = swap_root_dir();
if (!buf.Filename().empty()) {
return compute_swap_path_for_filename(buf.Filename());
}
// Unnamed buffers: unique within the process.
static std::atomic<std::uint64_t> ctr{0};
const std::uint64_t n = ++ctr;
const std::string name = "unnamed-" + std::to_string(n) + ".swp";
return (root / name).string();
}
std::string
SwapManager::ComputeSwapPathForFilename(const std::string &filename)
{
return ComputeSidecarPathForFilename(filename);
}
std::string
SwapManager::ComputeSidecarPathForFilename(const std::string &filename)
{
return compute_swap_path_for_filename(filename);
}
std::uint64_t
SwapManager::now_ns()
{
using namespace std::chrono;
return duration_cast<nanoseconds>(steady_clock::now().time_since_epoch()).count();
}
bool
SwapManager::ensure_parent_dir(const std::string &path)
{
try {
fs::path p(path);
fs::path dir = p.parent_path();
if (dir.empty())
return true;
if (!fs::exists(dir))
fs::create_directories(dir);
return true;
} catch (...) {
return false;
}
}
bool
SwapManager::write_header(int fd)
{
if (fd < 0)
return false;
// Fixed 64-byte header (v1)
// [magic 8][version u32][flags u32][created_time u64][reserved/padding]
std::uint8_t hdr[64];
std::memset(hdr, 0, sizeof(hdr));
std::memcpy(hdr, MAGIC, 8);
// version (little-endian)
hdr[8] = static_cast<std::uint8_t>(VERSION & 0xFFu);
hdr[9] = static_cast<std::uint8_t>((VERSION >> 8) & 0xFFu);
hdr[10] = static_cast<std::uint8_t>((VERSION >> 16) & 0xFFu);
hdr[11] = static_cast<std::uint8_t>((VERSION >> 24) & 0xFFu);
// flags = 0
// created_time (unix seconds; little-endian)
std::uint64_t ts = static_cast<std::uint64_t>(std::time(nullptr));
put_le64(hdr + 16, ts);
return write_full(fd, hdr, sizeof(hdr));
}
bool
SwapManager::open_ctx(JournalCtx &ctx, const std::string &path, std::string &err)
{
err.clear();
if (ctx.fd >= 0)
return true;
if (!ensure_parent_dir(path)) {
err = "Failed to create parent directory for swap file: " + path;
return false;
}
int flags = O_CREAT | O_WRONLY | O_APPEND;
#ifdef O_CLOEXEC
flags |= O_CLOEXEC;
#endif
// Retry on transient errors (ENOSPC, EDQUOT, EBUSY, etc.)
int fd = -1;
auto open_fn = [&]() -> bool {
fd = kte::syscall::Open(path.c_str(), flags, 0600);
return fd >= 0;
};
if (!RetryOnTransientError(open_fn, RetryPolicy::Aggressive(), err)) {
if (fd < 0) {
int saved_errno = errno;
err = "Failed to open swap file '" + path + "': " + std::strerror(saved_errno) + err;
}
return false;
}
// Ensure permissions even if file already existed.
(void) kte::syscall::Fchmod(fd, 0600);
struct stat st{};
if (kte::syscall::Fstat(fd, &st) != 0) {
int saved_errno = errno;
kte::syscall::Close(fd);
err = "Failed to fstat swap file '" + path + "': " + std::strerror(saved_errno);
return false;
}
// If an existing file is too small to contain the fixed header, truncate
// and restart.
if (st.st_size > 0 && st.st_size < 64) {
kte::syscall::Close(fd);
int tflags = O_CREAT | O_WRONLY | O_TRUNC | O_APPEND;
#ifdef O_CLOEXEC
tflags |= O_CLOEXEC;
#endif
// Retry on transient errors for truncation open
fd = -1;
auto reopen_fn = [&]() -> bool {
fd = kte::syscall::Open(path.c_str(), tflags, 0600);
return fd >= 0;
};
if (!RetryOnTransientError(reopen_fn, RetryPolicy::Aggressive(), err)) {
if (fd < 0) {
int saved_errno = errno;
err = "Failed to reopen swap file for truncation '" + path + "': " + std::strerror(
saved_errno) + err;
}
return false;
}
(void) kte::syscall::Fchmod(fd, 0600);
st.st_size = 0;
}
ctx.fd = fd;
ctx.path = path;
if (st.st_size == 0) {
ctx.header_ok = write_header(fd);
ctx.approx_size_bytes = ctx.header_ok ? 64 : 0;
if (!ctx.header_ok) {
err = "Failed to write swap file header: " + path;
// Drop any partial header and close, so the next attempt starts a
// fresh file instead of appending records behind a torn header.
(void) kte::syscall::Ftruncate(fd, 0);
kte::syscall::Close(fd);
ctx.fd = -1;
}
} else {
ctx.header_ok = true; // stage 1: trust existing header
ctx.approx_size_bytes = static_cast<std::uint64_t>(st.st_size);
}
return ctx.header_ok;
}
void
SwapManager::close_ctx(JournalCtx &ctx)
{
if (ctx.fd >= 0) {
(void) kte::syscall::Fsync(ctx.fd);
kte::syscall::Close(ctx.fd);
ctx.fd = -1;
}
ctx.header_ok = false;
}
bool
SwapManager::compact_to_checkpoint(JournalCtx &ctx, const std::vector<std::uint8_t> &chkpt_record, std::string &err)
{
err.clear();
if (ctx.path.empty()) {
err = "Compact failed: empty path";
return false;
}
if (chkpt_record.empty()) {
err = "Compact failed: empty checkpoint record";
return false;
}
// Close existing file before rename.
if (ctx.fd >= 0) {
(void) kte::syscall::Fsync(ctx.fd);
kte::syscall::Close(ctx.fd);
ctx.fd = -1;
}
ctx.header_ok = false;
const std::string tmp_path = ctx.path + ".tmp";
// Create the compacted file: header + checkpoint record.
if (!ensure_parent_dir(tmp_path)) {
err = "Failed to create parent directory for temp swap file: " + tmp_path;
return false;
}
int flags = O_CREAT | O_WRONLY | O_TRUNC;
#ifdef O_CLOEXEC
flags |= O_CLOEXEC;
#endif
// Retry on transient errors for temp file creation
int tfd = -1;
auto open_tmp_fn = [&]() -> bool {
tfd = kte::syscall::Open(tmp_path.c_str(), flags, 0600);
return tfd >= 0;
};
if (!RetryOnTransientError(open_tmp_fn, RetryPolicy::Aggressive(), err)) {
if (tfd < 0) {
int saved_errno = errno;
err = "Failed to open temp swap file '" + tmp_path + "': " + std::strerror(saved_errno) + err;
}
return false;
}
(void) kte::syscall::Fchmod(tfd, 0600);
bool ok = write_header(tfd);
if (ok)
ok = write_full(tfd, chkpt_record.data(), chkpt_record.size());
if (ok) {
if (kte::syscall::Fsync(tfd) != 0) {
int saved_errno = errno;
err = "Failed to fsync temp swap file '" + tmp_path + "': " + std::strerror(saved_errno);
ok = false;
}
}
kte::syscall::Close(tfd);
if (!ok) {
if (err.empty()) {
err = "Failed to write temp swap file: " + tmp_path;
}
std::remove(tmp_path.c_str());
return false;
}
// Atomic replace.
if (::rename(tmp_path.c_str(), ctx.path.c_str()) != 0) {
int saved_errno = errno;
err = "Failed to rename temp swap file '" + tmp_path + "' to '" + ctx.path + "': " + std::strerror(
saved_errno);
std::remove(tmp_path.c_str());
return false;
}
// Best-effort: fsync parent dir to persist the rename.
try {
fs::path p(ctx.path);
fs::path dir = p.parent_path();
if (!dir.empty()) {
int dflags = O_RDONLY;
#ifdef O_DIRECTORY
dflags |= O_DIRECTORY;
#endif
int dfd = kte::syscall::Open(dir.string().c_str(), dflags);
if (dfd >= 0) {
(void) kte::syscall::Fsync(dfd);
kte::syscall::Close(dfd);
}
}
} catch (...) {
// ignore
}
// Re-open for further appends.
if (!open_ctx(ctx, ctx.path, err)) {
// err already set by open_ctx
return false;
}
ctx.approx_size_bytes = 64 + static_cast<std::uint64_t>(chkpt_record.size());
return true;
}
std::uint32_t
SwapManager::crc32(const std::uint8_t *data, std::size_t len, std::uint32_t seed)
{
// Built once, thread-safely (used by the writer thread and by replay on
// the main thread; the old lazy init with a plain bool was a data race).
static const std::array<std::uint32_t, 256> table = [] {
std::array<std::uint32_t, 256> t{};
for (std::uint32_t i = 0; i < 256; ++i) {
std::uint32_t c = i;
for (int j = 0; j < 8; ++j)
c = (c & 1) ? (0xEDB88320u ^ (c >> 1)) : (c >> 1);
t[i] = c;
}
return t;
}();
std::uint32_t c = ~seed;
for (std::size_t i = 0; i < len; ++i)
c = table[(c ^ data[i]) & 0xFFu] ^ (c >> 8);
return ~c;
}
void
SwapManager::put_le32(std::vector<std::uint8_t> &out, std::uint32_t v)
{
out.push_back(static_cast<std::uint8_t>(v & 0xFFu));
out.push_back(static_cast<std::uint8_t>((v >> 8) & 0xFFu));
out.push_back(static_cast<std::uint8_t>((v >> 16) & 0xFFu));
out.push_back(static_cast<std::uint8_t>((v >> 24) & 0xFFu));
}
void
SwapManager::put_le64(std::uint8_t *dst, std::uint64_t v)
{
dst[0] = static_cast<std::uint8_t>(v & 0xFFu);
dst[1] = static_cast<std::uint8_t>((v >> 8) & 0xFFu);
dst[2] = static_cast<std::uint8_t>((v >> 16) & 0xFFu);
dst[3] = static_cast<std::uint8_t>((v >> 24) & 0xFFu);
dst[4] = static_cast<std::uint8_t>((v >> 32) & 0xFFu);
dst[5] = static_cast<std::uint8_t>((v >> 40) & 0xFFu);
dst[6] = static_cast<std::uint8_t>((v >> 48) & 0xFFu);
dst[7] = static_cast<std::uint8_t>((v >> 56) & 0xFFu);
}
void
SwapManager::put_u24_le(std::uint8_t dst[3], std::uint32_t v)
{
dst[0] = static_cast<std::uint8_t>(v & 0xFFu);
dst[1] = static_cast<std::uint8_t>((v >> 8) & 0xFFu);
dst[2] = static_cast<std::uint8_t>((v >> 16) & 0xFFu);
}
void
SwapManager::enqueue(Pending &&p)
{
{
std::lock_guard<std::mutex> lg(mtx_);
p.seq = ++next_seq_;
queue_.emplace_back(std::move(p));
}
cv_.notify_one();
}
void
SwapManager::RecordInsert(Buffer &buf, int row, int col, std::string_view text)
{
{
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(&buf);
if (it == journals_.end() || it->second.suspended)
return;
}
// payload v1: [encver u8=1][row u32][col u32][nbytes u32][bytes]
// A record holds at most kMaxRecordPayload bytes, so large inserts are
// split into consecutive INS records, each positioned after the last.
constexpr std::size_t kInsHeader = 13;
constexpr std::size_t kChunk = kMaxRecordPayload - kInsHeader;
std::size_t r = static_cast<std::size_t>(std::max(0, row));
std::size_t c = static_cast<std::size_t>(std::max(0, col));
std::size_t off = 0;
do {
const std::string_view chunk = text.substr(off, kChunk);
Pending p;
p.buf = &buf;
p.type = SwapRecType::INS;
p.payload.reserve(kInsHeader + chunk.size());
p.payload.push_back(1);
put_le32(p.payload, static_cast<std::uint32_t>(r));
put_le32(p.payload, static_cast<std::uint32_t>(c));
put_le32(p.payload, static_cast<std::uint32_t>(chunk.size()));
p.payload.insert(p.payload.end(), reinterpret_cast<const std::uint8_t *>(chunk.data()),
reinterpret_cast<const std::uint8_t *>(chunk.data()) + chunk.size());
enqueue(std::move(p));
// Advance (r, c) past the chunk for the next record.
const std::size_t last_nl = chunk.rfind('\n');
if (last_nl == std::string_view::npos) {
c += chunk.size();
} else {
r += static_cast<std::size_t>(std::count(chunk.begin(), chunk.end(), '\n'));
c = chunk.size() - last_nl - 1;
}
off += chunk.size();
} while (off < text.size());
maybe_request_checkpoint(buf, text.size());
}
void
SwapManager::RecordDelete(Buffer &buf, int row, int col, std::size_t len)
{
{
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(&buf);
if (it == journals_.end() || it->second.suspended)
return;
}
if (len > 0xFFFFFFFFu)
return;
Pending p;
p.buf = &buf;
p.type = SwapRecType::DEL;
// payload v1: [encver u8=1][row u32][col u32][len u32]
p.payload.push_back(1);
put_le32(p.payload, static_cast<std::uint32_t>(std::max(0, row)));
put_le32(p.payload, static_cast<std::uint32_t>(std::max(0, col)));
put_le32(p.payload, static_cast<std::uint32_t>(len));
enqueue(std::move(p));
maybe_request_checkpoint(buf, len);
}
void
SwapManager::RecordSplit(Buffer &buf, int row, int col)
{
{
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(&buf);
if (it == journals_.end() || it->second.suspended)
return;
}
Pending p;
p.buf = &buf;
p.type = SwapRecType::SPLIT;
// payload v1: [encver u8=1][row u32][col u32]
p.payload.push_back(1);
put_le32(p.payload, static_cast<std::uint32_t>(std::max(0, row)));
put_le32(p.payload, static_cast<std::uint32_t>(std::max(0, col)));
enqueue(std::move(p));
maybe_request_checkpoint(buf, 1);
}
void
SwapManager::RecordJoin(Buffer &buf, int row)
{
{
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(&buf);
if (it == journals_.end() || it->second.suspended)
return;
}
Pending p;
p.buf = &buf;
p.type = SwapRecType::JOIN;
// payload v1: [encver u8=1][row u32]
p.payload.push_back(1);
put_le32(p.payload, static_cast<std::uint32_t>(std::max(0, row)));
enqueue(std::move(p));
maybe_request_checkpoint(buf, 1);
}
void
SwapManager::maybe_request_checkpoint(Buffer &buf, const std::size_t approx_edit_bytes)
{
SwapConfig cfg;
bool do_chkpt = false;
{
std::lock_guard<std::mutex> lg(mtx_);
cfg = cfg_;
auto it = journals_.find(&buf);
if (it == journals_.end() || it->second.suspended)
return;
if (cfg.checkpoint_bytes == 0 && cfg.checkpoint_interval_ms == 0 && !it->second.gap)
return;
JournalCtx &ctx = it->second;
ctx.edit_bytes_since_chkpt += approx_edit_bytes;
const std::uint64_t now = now_ns();
if (ctx.last_chkpt_ns == 0)
ctx.last_chkpt_ns = now;
const bool bytes_hit = (cfg.checkpoint_bytes > 0) && (
ctx.edit_bytes_since_chkpt >= cfg.checkpoint_bytes);
const bool time_hit = (cfg.checkpoint_interval_ms > 0) &&
(((now - ctx.last_chkpt_ns) / 1000000ULL) >= cfg.checkpoint_interval_ms);
const bool gap_hit = ctx.gap && (now - ctx.gap_chkpt_request_ns) >= kGapCheckpointIntervalNs;
if (gap_hit)
ctx.gap_chkpt_request_ns = now;
if (bytes_hit || time_hit || gap_hit) {
ctx.edit_bytes_since_chkpt = 0;
ctx.last_chkpt_ns = now;
do_chkpt = true;
}
}
if (do_chkpt) {
RecordCheckpoint(buf, false);
}
}
void
SwapManager::RecordCheckpoint(Buffer &buf, const bool urgent_flush)
{
{
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(&buf);
if (it == journals_.end() || it->second.suspended)
return;
}
// payload = encver(1) + nbytes(4) + bytes; larger buffers cannot be
// checkpointed in this record format (the writer would reject them), so
// check the size before paying for a snapshot.
const std::size_t nrows = buf.Nrows();
const std::size_t total_size = nrows ? buf.GetLineRange(nrows - 1).second : 0;
if (total_size > kMaxRecordPayload - 5) {
// If the journal has a gap, only a checkpoint could close it: say so
// once rather than silently dropping every later record.
bool report = false;
{
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(&buf);
if (it != journals_.end() && it->second.gap && !it->second.gap_unfixable_reported) {
it->second.gap_unfixable_reported = true;
report = true;
}
}
if (report)
report_error("Swap journal lost a record and the buffer is too large to checkpoint; "
"crash recovery will not include edits made until the file is saved", &buf);
return;
}
const std::string bytes = snapshot_buffer_bytes(buf);
if (bytes.size() > kMaxRecordPayload - 5)
return;
Pending p;
p.buf = &buf;
p.type = SwapRecType::CHKPT;
p.urgent_flush = urgent_flush;
// payload v1: [encver u8=1][nbytes u32][bytes]
p.payload.push_back(1);
put_le32(p.payload, static_cast<std::uint32_t>(bytes.size()));
p.payload.insert(p.payload.end(), reinterpret_cast<const std::uint8_t *>(bytes.data()),
reinterpret_cast<const std::uint8_t *>(bytes.data()) + bytes.size());
enqueue(std::move(p));
}
void
SwapManager::writer_loop()
{
for (;;) {
std::vector<Pending> batch;
{
std::unique_lock<std::mutex> lk(mtx_);
if (queue_.empty()) {
if (!running_.load())
break;
cv_.wait_for(lk, std::chrono::milliseconds(cfg_.flush_interval_ms));
}
if (!queue_.empty()) {
batch.swap(queue_);
inflight_ += batch.size();
}
}
if (batch.empty())
continue;
for (const Pending &p: batch) {
try {
process_one(p);
} catch (const std::exception &e) {
report_error(std::string("Exception in process_one: ") + e.what(), p.buf);
} catch (...) {
report_error("Unknown exception in process_one", p.buf);
}
{
std::lock_guard<std::mutex> lg(mtx_);
if (p.seq > last_processed_)
last_processed_ = p.seq;
if (inflight_ > 0)
--inflight_;
}
cv_.notify_all();
}
// Throttled fsync: best-effort (grouped)
try {
std::vector<int> to_sync;
std::uint64_t now = now_ns();
{
std::lock_guard<std::mutex> lg(mtx_);
for (auto &kv: journals_) {
JournalCtx &ctx = kv.second;
if (ctx.fd >= 0) {
if (ctx.last_fsync_ns == 0 || (now - ctx.last_fsync_ns) / 1000000ULL >=
cfg_.fsync_interval_ms) {
ctx.last_fsync_ns = now;
to_sync.push_back(ctx.fd);
}
}
}
}
for (int fd: to_sync) {
(void) kte::syscall::Fsync(fd);
}
} catch (const std::exception &e) {
report_error(std::string("Exception in fsync operations: ") + e.what());
} catch (...) {
report_error("Unknown exception in fsync operations");
}
}
// Wake any waiters.
cv_.notify_all();
}
void
SwapManager::process_one(const Pending &p)
{
if (!p.buf)
return;
// Any record that is not written leaves a gap in the journal.
auto mark_gap = [&]() {
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(p.buf);
if (it != journals_.end())
it->second.gap = true;
};
// Check circuit breaker before processing
bool circuit_open = false;
{
std::lock_guard<std::mutex> lg(mtx_);
if (!circuit_breaker_.AllowRequest()) {
circuit_open = true;
}
}
if (circuit_open) {
// Circuit is open - graceful degradation: skip swap write
// This prevents repeated failures from overwhelming the system
// Swap recording will resume when circuit closes
static std::atomic<std::uint64_t> last_warning_ns{0};
const std::uint64_t now = now_ns();
const std::uint64_t last = last_warning_ns.load();
// Log warning at most once per 60 seconds to avoid spam
if (now - last > 60000000000ULL) {
last_warning_ns.store(now);
// Journal path read under mtx_, not Buffer::Filename(), which the
// main thread may be assigning (see report_error).
std::string context = "<unnamed>";
{
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(p.buf);
if (it != journals_.end() && !it->second.path.empty())
context = it->second.path;
}
ErrorHandler::Instance().Warning("SwapManager",
"Swap operations temporarily disabled due to repeated failures (circuit breaker open)",
context);
}
mark_gap();
return;
}
try {
Buffer &buf = *p.buf;
JournalCtx *ctxp = nullptr;
std::string path;
std::size_t compact_bytes = 0;
{
std::lock_guard<std::mutex> lg(mtx_);
auto it = journals_.find(p.buf);
if (it == journals_.end())
return;
// After a gap only a checkpoint can bring the journal back in step.
if (it->second.gap && p.type != SwapRecType::CHKPT)
return;
if (it->second.path.empty())
it->second.path = ComputeSidecarPath(buf);
path = it->second.path;
ctxp = &it->second;
compact_bytes = cfg_.compact_bytes;
}
if (!ctxp)
return;
std::string open_err;
if (!open_ctx(*ctxp, path, open_err)) {
report_error(open_err, p.buf);
{
std::lock_guard<std::mutex> lg(mtx_);
circuit_breaker_.RecordFailure();
}
mark_gap();
return;
}
if (p.payload.size() > kMaxRecordPayload) {
// Recorders keep payloads within the limit, so this is a bug, not an
// I/O failure: do not count it against the circuit breaker.
report_error("Payload too large: " + std::to_string(p.payload.size()) + " bytes", p.buf);
mark_gap();
return;
}
// Build record: [type u8][len u24][payload][crc32 u32]
std::uint8_t len3[3];
put_u24_le(len3, static_cast<std::uint32_t>(p.payload.size()));
std::uint8_t head[4];
head[0] = static_cast<std::uint8_t>(p.type);
head[1] = len3[0];
head[2] = len3[1];
head[3] = len3[2];
std::uint32_t c = 0;
c = crc32(head, sizeof(head), c);
if (!p.payload.empty())
c = crc32(p.payload.data(), p.payload.size(), c);
std::uint8_t crcbytes[4];
crcbytes[0] = static_cast<std::uint8_t>(c & 0xFFu);
crcbytes[1] = static_cast<std::uint8_t>((c >> 8) & 0xFFu);
crcbytes[2] = static_cast<std::uint8_t>((c >> 16) & 0xFFu);
crcbytes[3] = static_cast<std::uint8_t>((c >> 24) & 0xFFu);
std::vector<std::uint8_t> rec;
rec.reserve(sizeof(head) + p.payload.size() + sizeof(crcbytes));
rec.insert(rec.end(), head, head + sizeof(head));
if (!p.payload.empty())
rec.insert(rec.end(), p.payload.begin(), p.payload.end());
rec.insert(rec.end(), crcbytes, crcbytes + sizeof(crcbytes));
// Remember where this record starts so a partial write can be undone;
// otherwise later records would be appended behind the torn bytes.
struct stat before{};
const bool have_size = kte::syscall::Fstat(ctxp->fd, &before) == 0;
// Write (handle partial writes and check results)
bool ok = write_full(ctxp->fd, rec.data(), rec.size());
if (!ok) {
int err = errno;
report_error("Failed to write swap record to '" + path + "': " + std::strerror(err), p.buf);
if (have_size)
(void) kte::syscall::Ftruncate(ctxp->fd, before.st_size);
{
std::lock_guard<std::mutex> lg(mtx_);
circuit_breaker_.RecordFailure();
}
mark_gap();
return;
}
if (p.type == SwapRecType::CHKPT) {
std::lock_guard<std::mutex> lg(mtx_);
ctxp->gap = false;
}
ctxp->approx_size_bytes += static_cast<std::uint64_t>(rec.size());
if (p.urgent_flush) {
if (kte::syscall::Fsync(ctxp->fd) != 0) {
int err = errno;
report_error("Failed to fsync swap file '" + path + "': " + std::strerror(err), p.buf);
}
ctxp->last_fsync_ns = now_ns();
}
if (p.type == SwapRecType::CHKPT && compact_bytes > 0 &&
ctxp->approx_size_bytes >= static_cast<std::uint64_t>(compact_bytes)) {
std::string compact_err;
if (!compact_to_checkpoint(*ctxp, rec, compact_err)) {
report_error(compact_err, p.buf);
// Note: compaction failure is not fatal, don't record circuit breaker failure
}
}
// Record success for circuit breaker
{
std::lock_guard<std::mutex> lg(mtx_);
circuit_breaker_.RecordSuccess();
}
} catch (const std::exception &e) {
report_error(std::string("Exception in process_one: ") + e.what(), p.buf);
{
std::lock_guard<std::mutex> lg(mtx_);
circuit_breaker_.RecordFailure();
}
mark_gap();
} catch (...) {
report_error("Unknown exception in process_one", p.buf);
{
std::lock_guard<std::mutex> lg(mtx_);
circuit_breaker_.RecordFailure();
}
mark_gap();
}
}
static bool
read_exact(std::ifstream &in, void *dst, std::size_t n)
{
in.read(static_cast<char *>(dst), static_cast<std::streamsize>(n));
return in.good() && static_cast<std::size_t>(in.gcount()) == n;
}
static std::uint32_t
read_le32(const std::uint8_t b[4])
{
return (std::uint32_t) b[0] | ((std::uint32_t) b[1] << 8) | ((std::uint32_t) b[2] << 16) | (
(std::uint32_t) b[3] << 24);
}
static bool
parse_u32_le(const std::vector<std::uint8_t> &p, std::size_t &off, std::uint32_t &out)
{
if (off + 4 > p.size())
return false;
out = (std::uint32_t) p[off] | ((std::uint32_t) p[off + 1] << 8) | ((std::uint32_t) p[off + 2] << 16) |
((std::uint32_t) p[off + 3] << 24);
off += 4;
return true;
}
bool
SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &err,
std::uint64_t *valid_bytes)
{
err.clear();
std::ifstream in(swap_path, std::ios::binary);
if (!in) {
err = "Failed to open swap file for replay: " + swap_path;
return false;
}
std::uint8_t hdr[64];
if (!read_exact(in, hdr, sizeof(hdr))) {
err = "Swap file truncated (header): " + swap_path;
return false;
}
if (std::memcmp(hdr, MAGIC, 8) != 0) {
err = "Swap file has bad magic: " + swap_path;
return false;
}
const std::uint32_t ver = read_le32(hdr + 8);
if (ver != VERSION) {
err = "Unsupported swap version: " + std::to_string(ver);
return false;
}
// Ensure replayed edits don't get re-journaled if the caller forgot to detach/suspend.
kte::SwapRecorder *prev_rec = buf.SwapRecorder();
buf.SetSwapRecorder(nullptr);
struct RestoreSwapRecorder {
Buffer &b;
kte::SwapRecorder *prev;
~RestoreSwapRecorder()
{
b.SetSwapRecorder(prev);
}
} restore{buf, prev_rec};
for (;;) {
// Everything before this offset replayed cleanly.
const std::streamoff record_start = in.tellg();
if (valid_bytes && record_start >= 0)
*valid_bytes = static_cast<std::uint64_t>(record_start);
std::uint8_t head[4];
in.read(reinterpret_cast<char *>(head), sizeof(head));
const std::size_t got_head = static_cast<std::size_t>(in.gcount());
if (got_head == 0 && in.eof()) {
return true; // clean EOF
}
// A record cut short by end-of-file is the expected result of a crash or
// power loss mid-append. Keep everything replayed so far and stop; the
// caller still sees err (with a true result) and can report it.
auto torn_tail = [&](const char *what) {
if (in.bad()) {
err = std::string("Failed to read swap file (") + what + "): " + swap_path;
return false;
}
err = std::string("Swap file ends with an incomplete record (") + what + "); recovered the records before it: " +
swap_path;
kte::ErrorHandler::Instance().Warning("SwapManager", err, swap_path);
return true;
};
if (got_head != sizeof(head))
return torn_tail("record header");
const SwapRecType type = static_cast<SwapRecType>(head[0]);
const std::size_t len = (std::size_t) head[1] | ((std::size_t) head[2] << 8) | (
(std::size_t) head[3] << 16);
std::vector<std::uint8_t> payload;
payload.resize(len);
if (len > 0 && !read_exact(in, payload.data(), len))
return torn_tail("payload");
std::uint8_t crcbytes[4];
if (!read_exact(in, crcbytes, sizeof(crcbytes)))
return torn_tail("crc");
const std::uint32_t want_crc = read_le32(crcbytes);
std::uint32_t got_crc = 0;
got_crc = crc32(head, sizeof(head), got_crc);
if (!payload.empty())
got_crc = crc32(payload.data(), payload.size(), got_crc);
if (got_crc != want_crc) {
err = "Swap file CRC mismatch: " + swap_path;
return false;
}
// Apply record
switch (type) {
case SwapRecType::INS: {
std::size_t off = 0;
// INS payload: encver(1) + row(4) + col(4) + nbytes(4) + data(nbytes)
// Minimum: 1 + 4 + 4 + 4 = 13 bytes
if (payload.size() < 13) {
err = "INS payload too short (need at least 13 bytes)";
return false;
}
const std::uint8_t encver = payload[off++];
if (encver != 1) {
err = "Unsupported swap payload encoding";
return false;
}
std::uint32_t row = 0, col = 0, nbytes = 0;
if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col) || !parse_u32_le(
payload, off, nbytes)) {
err = "Malformed INS payload (failed to parse row/col/nbytes)";
return false;
}
if (off + nbytes > payload.size()) {
err = "Truncated INS payload bytes";
return false;
}
buf.insert_text((int) row, (int) col,
std::string_view(reinterpret_cast<const char *>(payload.data() + off), nbytes));
break;
}
case SwapRecType::DEL: {
std::size_t off = 0;
// DEL payload: encver(1) + row(4) + col(4) + dlen(4)
// Minimum: 1 + 4 + 4 + 4 = 13 bytes
if (payload.size() < 13) {
err = "DEL payload too short (need at least 13 bytes)";
return false;
}
const std::uint8_t encver = payload[off++];
if (encver != 1) {
err = "Unsupported swap payload encoding";
return false;
}
std::uint32_t row = 0, col = 0, dlen = 0;
if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col) || !parse_u32_le(
payload, off, dlen)) {
err = "Malformed DEL payload (failed to parse row/col/dlen)";
return false;
}
buf.delete_text((int) row, (int) col, (std::size_t) dlen);
break;
}
case SwapRecType::SPLIT: {
std::size_t off = 0;
// SPLIT payload: encver(1) + row(4) + col(4)
// Minimum: 1 + 4 + 4 = 9 bytes
if (payload.size() < 9) {
err = "SPLIT payload too short (need at least 9 bytes)";
return false;
}
const std::uint8_t encver = payload[off++];
if (encver != 1) {
err = "Unsupported swap payload encoding";
return false;
}
std::uint32_t row = 0, col = 0;
if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col)) {
err = "Malformed SPLIT payload (failed to parse row/col)";
return false;
}
buf.split_line((int) row, (int) col);
break;
}
case SwapRecType::JOIN: {
std::size_t off = 0;
// JOIN payload: encver(1) + row(4)
// Minimum: 1 + 4 = 5 bytes
if (payload.size() < 5) {
err = "JOIN payload too short (need at least 5 bytes)";
return false;
}
const std::uint8_t encver = payload[off++];
if (encver != 1) {
err = "Unsupported swap payload encoding";
return false;
}
std::uint32_t row = 0;
if (!parse_u32_le(payload, off, row)) {
err = "Malformed JOIN payload (failed to parse row)";
return false;
}
buf.join_lines((int) row);
break;
}
case SwapRecType::CHKPT: {
std::size_t off = 0;
// CHKPT payload: encver(1) + nbytes(4) + data(nbytes)
// Minimum: 1 + 4 = 5 bytes
if (payload.size() < 5) {
err = "CHKPT payload too short (need at least 5 bytes)";
return false;
}
const std::uint8_t encver = payload[off++];
if (encver != 1) {
err = "Unsupported swap checkpoint encoding";
return false;
}
std::uint32_t nbytes = 0;
if (!parse_u32_le(payload, off, nbytes)) {
err = "Malformed CHKPT payload (failed to parse nbytes)";
return false;
}
if (off + nbytes > payload.size()) {
err = "Truncated CHKPT payload bytes";
return false;
}
buf.replace_all_bytes(std::string_view(reinterpret_cast<const char *>(payload.data() + off),
(std::size_t) nbytes));
break;
}
default:
// Ignore unknown types for forward-compat
break;
}
}
}
void
SwapManager::report_error(const std::string &message, Buffer *buf)
{
// Identify the buffer by its journal path, read under mtx_. This runs on
// the writer thread, where reading Buffer::Filename() raced with the main
// thread assigning it (SaveAs, OpenFromFile).
std::string context = "<unknown>";
{
std::lock_guard<std::mutex> lg(mtx_);
if (buf) {
auto it = journals_.find(buf);
context = (it != journals_.end() && !it->second.path.empty()) ? it->second.path : "<unnamed>";
}
}
// Report to centralized error handler
ErrorHandler::Instance().Error("SwapManager", message, context);
// Maintain local error tracking for backward compatibility
std::lock_guard<std::mutex> lg(mtx_);
SwapError err;
err.timestamp_ns = now_ns();
err.message = message;
err.buffer_name = context;
errors_.push_back(err);
// Bound the error queue to 100 entries
while (errors_.size() > 100) {
errors_.pop_front();
}
++total_error_count_;
}
bool
SwapManager::HasErrors() const
{
std::lock_guard<std::mutex> lg(mtx_);
return !errors_.empty();
}
std::string
SwapManager::GetLastError() const
{
std::lock_guard<std::mutex> lg(mtx_);
if (errors_.empty())
return "";
const SwapError &e = errors_.back();
return "[" + e.buffer_name + "] " + e.message;
}
std::size_t
SwapManager::GetErrorCount() const
{
std::lock_guard<std::mutex> lg(mtx_);
return total_error_count_;
}
} // namespace kte