* test(storage): add crash-replay and handle-failure coverage for ReferenceCounter Adds 6 test cases: - Recovery after forced close preserves committed data - Uncommitted transaction rolled back after forced close - Corrupted database file rejected on open - Consistent stats after interrupted commit recovery - Multiple PENDING transactions survive force close - WAL checkpoint survives forced close * fix(ci): remove ccache — conflicts with OpenBLAS ASM as direct compiler * fix(ci): use explicit CC/CXX paths to prevent Conan from using ccache directly ccache was picked up by Conan/CMake as the compiler itself (/usr/bin/ccache) rather than as a transparent wrapper, causing OpenBLAS ASM compilation to fail with 'ccache: invalid option -- D'. Setting explicit compiler paths prevents this. * fix(ci): add YAMS_SKIP_CCACHE to avoid OpenBLAS ASM ccache conflict setup.sh wraps CC/CXX with ccache for faster incremental builds, but this causes Conan/CMake to use ccache directly as the ASM compiler for OpenBLAS, where it fails on -D flags. Add an opt-out gate in setup.sh and set YAMS_SKIP_CCACHE=1 in .build.yml for sourcehut CI VMs. * fix(ci): add libomp-dev, libfaiss-dev, libopenblas-dev for FAISS build * fix(ci): limit meson compile to -j1 to prevent OOM on sourcehut VMs * fix(build): restore hooks and shrink srht Compile the storage library with YAMS_TESTING in test builds so debug-only hooks link again. Include the POSIX chmod header used by the storage utility Catch2 coverage cases. Prune SourceHut fallback stage archives to runtime files so branch artifacts stay under the upload limit. * refactor(storage): drop dead code, add coverage * refactor(cli): polish grep/search graph UX * fix(daemon): stabilize shutdown and CI paths - force vector-backed tests to opt out of CI-wide disable flags - reset reference prune statements between deletes - make optional-plugin and POSIX-only test assumptions portable - extract ServiceManager open/shutdown helpers for safer WAL recovery work * test(daemon): add db vacuum coverage * test(daemon): add WAL startup recovery - cover stale WAL startup recovery in ServiceManager - cover repeated restart cycles with seeded WAL sidecars - assert daemon recovery reporting for corrupt data-dir DBs - verify oversized metadata DBs shrink after WAL recovery shutdown * refactor(grep): consolidate indexed content scan * fix(grep): remove pre-scan candidate truncation * refactor(runtime): split startup, harden storage * feat(bench): add manifest retrieval corpus * fix(bench): run fusion experiment on simeon * fix(cli): sharpen search graph hints * fix(build): refresh version metadata * fix(ci): stabilize sync search and srht packaging * fix(ci): unblock windows and shrink artifacts * fix(ci): stabilize linux runtime checks * fix(build): shrink srht manifest
242 lines
7.9 KiB
C++
242 lines
7.9 KiB
C++
// Copyright (c) 2026 YAMS Contributors
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
//
|
|
// Tests for auto-VACUUM during database startup to prevent file bloat
|
|
// from repeated crash-recovery cycles.
|
|
//
|
|
// PBI: auto-vacuum-bloat
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include <yams/metadata/database.h>
|
|
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace {
|
|
|
|
fs::path makeScratchDir(const std::string& prefix) {
|
|
auto base = fs::temp_directory_path() /
|
|
(prefix + "_" +
|
|
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
|
fs::create_directories(base);
|
|
return base;
|
|
}
|
|
|
|
// Create a DB with insert/delete cycles to bloat the file with free pages.
|
|
std::uintmax_t bloatDatabase(yams::metadata::Database& db, const fs::path& dbPath,
|
|
int numCycles = 5, int rowsPerCycle = 5000) {
|
|
REQUIRE(db.open(dbPath.string(), yams::metadata::ConnectionMode::Create));
|
|
REQUIRE(db.execute("CREATE TABLE IF NOT EXISTS bloated(x INTEGER PRIMARY KEY, y TEXT)"));
|
|
|
|
for (int cycle = 0; cycle < numCycles; ++cycle) {
|
|
REQUIRE(db.execute("BEGIN"));
|
|
for (int i = 0; i < rowsPerCycle; ++i) {
|
|
auto sql =
|
|
"INSERT INTO bloated VALUES(" + std::to_string(cycle * rowsPerCycle + i) +
|
|
", 'padding_data_to_make_pages_larger_abcdefghijklmnopqrstuvwxyz0123456789')";
|
|
REQUIRE(db.execute(sql));
|
|
}
|
|
REQUIRE(db.execute("COMMIT"));
|
|
|
|
// Delete most rows leaving free pages
|
|
REQUIRE(db.execute("BEGIN"));
|
|
auto delRange = "DELETE FROM bloated WHERE x < " +
|
|
std::to_string(cycle * rowsPerCycle + rowsPerCycle - 1);
|
|
REQUIRE(db.execute(delRange));
|
|
REQUIRE(db.execute("COMMIT"));
|
|
}
|
|
|
|
// Keep a few rows
|
|
auto lastCycle = numCycles;
|
|
REQUIRE(db.execute("INSERT INTO bloated VALUES(" +
|
|
std::to_string(lastCycle * rowsPerCycle + 1) + ", 'survivor_row')"));
|
|
|
|
db.close();
|
|
|
|
std::error_code ec;
|
|
auto size = fs::file_size(dbPath, ec);
|
|
return ec ? 0 : size;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("VACUUM shrinks a bloated database file", "[unit][daemon][db_recovery][vacuum]") {
|
|
auto dir = makeScratchDir("yams_vacuum_shrink");
|
|
auto dbPath = dir / "yams.db";
|
|
|
|
std::uintmax_t sizeBefore = 0;
|
|
{
|
|
yams::metadata::Database db;
|
|
sizeBefore = bloatDatabase(db, dbPath, 5, 5000);
|
|
}
|
|
|
|
if (sizeBefore == 0) {
|
|
SUCCEED("Skipping — file_size unavailable on this filesystem");
|
|
std::error_code ec;
|
|
fs::remove_all(dir, ec);
|
|
return;
|
|
}
|
|
|
|
INFO("DB size before VACUUM: " << sizeBefore << " bytes");
|
|
REQUIRE(sizeBefore > 50'000);
|
|
|
|
// Run VACUUM
|
|
{
|
|
yams::metadata::Database db;
|
|
REQUIRE(db.open(dbPath.string(), yams::metadata::ConnectionMode::ReadWrite));
|
|
auto vacuumR = db.execute("VACUUM");
|
|
REQUIRE(vacuumR);
|
|
db.close();
|
|
}
|
|
|
|
std::error_code ec;
|
|
auto sizeAfter = fs::file_size(dbPath, ec);
|
|
REQUIRE_FALSE(ec);
|
|
INFO("DB size after VACUUM: " << sizeAfter << " bytes");
|
|
INFO("Size reduction: " << ((1.0 - static_cast<double>(sizeAfter) / sizeBefore) * 100.0)
|
|
<< "%");
|
|
|
|
// VACUUM must significantly shrink the file
|
|
REQUIRE(sizeAfter < sizeBefore / 2);
|
|
|
|
// Data must survive
|
|
{
|
|
yams::metadata::Database db;
|
|
REQUIRE(db.open(dbPath.string(), yams::metadata::ConnectionMode::ReadWrite));
|
|
auto stmtR = db.prepare("SELECT COUNT(*) FROM bloated WHERE y = 'survivor_row'");
|
|
REQUIRE(stmtR);
|
|
auto& stmt = stmtR.value();
|
|
REQUIRE(stmt.step());
|
|
REQUIRE(stmt.getInt(0) == 1);
|
|
db.close();
|
|
}
|
|
|
|
fs::remove_all(dir, ec);
|
|
}
|
|
|
|
TEST_CASE("VACUUM preserves all table data", "[unit][daemon][db_recovery][vacuum]") {
|
|
auto dir = makeScratchDir("yams_vacuum_preserve");
|
|
auto dbPath = dir / "yams.db";
|
|
|
|
{
|
|
yams::metadata::Database db;
|
|
REQUIRE(db.open(dbPath.string(), yams::metadata::ConnectionMode::Create));
|
|
REQUIRE(db.execute("CREATE TABLE test_data(id INTEGER PRIMARY KEY, value TEXT)"));
|
|
REQUIRE(db.execute("INSERT INTO test_data VALUES(1, 'alpha')"));
|
|
REQUIRE(db.execute("INSERT INTO test_data VALUES(2, 'beta')"));
|
|
REQUIRE(db.execute("INSERT INTO test_data VALUES(3, 'gamma')"));
|
|
|
|
// Delete and re-insert to create free pages
|
|
REQUIRE(db.execute("DELETE FROM test_data WHERE id = 2"));
|
|
REQUIRE(db.execute("INSERT INTO test_data VALUES(2, 'beta_restored')"));
|
|
|
|
REQUIRE(db.execute("CREATE TABLE test_data2(a INTEGER, b REAL)"));
|
|
REQUIRE(db.execute("INSERT INTO test_data2 VALUES(100, 3.14)"));
|
|
REQUIRE(db.execute("INSERT INTO test_data2 VALUES(200, 2.718)"));
|
|
|
|
auto vacuumR = db.execute("VACUUM");
|
|
REQUIRE(vacuumR);
|
|
|
|
// Verify test_data rows
|
|
{
|
|
auto stmtR = db.prepare("SELECT id, value FROM test_data ORDER BY id");
|
|
REQUIRE(stmtR);
|
|
auto& stmt = stmtR.value();
|
|
REQUIRE(stmt.step());
|
|
REQUIRE(stmt.getInt(0) == 1);
|
|
REQUIRE(stmt.getString(1) == "alpha");
|
|
REQUIRE(stmt.step());
|
|
REQUIRE(stmt.getInt(0) == 2);
|
|
REQUIRE(stmt.getString(1) == "beta_restored");
|
|
REQUIRE(stmt.step());
|
|
REQUIRE(stmt.getInt(0) == 3);
|
|
REQUIRE(stmt.getString(1) == "gamma");
|
|
REQUIRE(!stmt.step().value());
|
|
}
|
|
// Verify test_data2 rows
|
|
{
|
|
auto stmtR = db.prepare("SELECT a, b FROM test_data2 ORDER BY a");
|
|
REQUIRE(stmtR);
|
|
auto& stmt = stmtR.value();
|
|
REQUIRE(stmt.step());
|
|
REQUIRE(stmt.getInt(0) == 100);
|
|
REQUIRE(std::abs(stmt.getDouble(1) - 3.14) < 0.001);
|
|
REQUIRE(stmt.step());
|
|
REQUIRE(stmt.getInt(0) == 200);
|
|
REQUIRE(std::abs(stmt.getDouble(1) - 2.718) < 0.001);
|
|
REQUIRE(!stmt.step().value());
|
|
}
|
|
|
|
db.close();
|
|
}
|
|
|
|
// Reopen and verify persistence
|
|
{
|
|
yams::metadata::Database db;
|
|
REQUIRE(db.open(dbPath.string(), yams::metadata::ConnectionMode::ReadWrite));
|
|
auto stmtR = db.prepare("SELECT COUNT(*) FROM test_data");
|
|
REQUIRE(stmtR);
|
|
auto& stmt = stmtR.value();
|
|
REQUIRE(stmt.step());
|
|
REQUIRE(stmt.getInt(0) == 3);
|
|
db.close();
|
|
}
|
|
|
|
std::error_code ec;
|
|
fs::remove_all(dir, ec);
|
|
}
|
|
|
|
TEST_CASE("VACUUM is safe on a tiny database", "[unit][daemon][db_recovery][vacuum]") {
|
|
auto dir = makeScratchDir("yams_vacuum_tiny");
|
|
auto dbPath = dir / "yams.db";
|
|
|
|
{
|
|
yams::metadata::Database db;
|
|
REQUIRE(db.open(dbPath.string(), yams::metadata::ConnectionMode::Create));
|
|
REQUIRE(db.execute("CREATE TABLE tiny(x INTEGER)"));
|
|
REQUIRE(db.execute("INSERT INTO tiny VALUES(1)"));
|
|
REQUIRE(db.execute("INSERT INTO tiny VALUES(2)"));
|
|
|
|
auto vacuumR = db.execute("VACUUM");
|
|
REQUIRE(vacuumR);
|
|
|
|
auto stmtR = db.prepare("SELECT COUNT(*) FROM tiny");
|
|
REQUIRE(stmtR);
|
|
auto& stmt = stmtR.value();
|
|
REQUIRE(stmt.step());
|
|
REQUIRE(stmt.getInt(0) == 2);
|
|
|
|
db.close();
|
|
}
|
|
|
|
std::error_code ec;
|
|
fs::remove_all(dir, ec);
|
|
}
|
|
|
|
TEST_CASE("VACUUM on empty database is safe", "[unit][daemon][db_recovery][vacuum]") {
|
|
auto dir = makeScratchDir("yams_vacuum_empty");
|
|
auto dbPath = dir / "yams.db";
|
|
|
|
{
|
|
yams::metadata::Database db;
|
|
REQUIRE(db.open(dbPath.string(), yams::metadata::ConnectionMode::Create));
|
|
// Opened but no tables/data
|
|
auto vacuumR = db.execute("VACUUM");
|
|
REQUIRE(vacuumR);
|
|
db.close();
|
|
}
|
|
|
|
std::error_code ec;
|
|
auto sizeAfter = fs::file_size(dbPath, ec);
|
|
REQUIRE_FALSE(ec);
|
|
INFO("Empty DB after VACUUM: " << sizeAfter << " bytes");
|
|
REQUIRE(sizeAfter <= 4096);
|
|
|
|
fs::remove_all(dir, ec);
|
|
}
|