mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Add post-dump plausibility gate returning status:degraded (#334)
Compare persisted SQLite node counts to in-memory dump counts after index_repository completes so partial WAL/durability loss surfaces as status:"degraded" instead of silent indexed. Signed-off-by: Sam Li <yangsec888@gmail.com>
This commit is contained in:
1 parent
acb7eaaa05
commit
559993a853
10 files changed
+439
-17
No files matched your search
+6
-3
@@ -111,7 +111,8 @@ FOUNDATION_SRCS = \
|
||||
src/foundation/compat_regex.c \
|
||||
src/foundation/mem.c \
|
||||
src/foundation/diagnostics.c \
|
||||
src/foundation/profile.c
|
||||
src/foundation/profile.c \
|
||||
src/foundation/dump_verify.c
|
||||
|
||||
# Existing extraction C code (compiled from current location)
|
||||
EXTRACTION_SRCS = \
|
||||
@@ -295,7 +296,8 @@ TEST_FOUNDATION_SRCS = \
|
||||
tests/test_str_intern.c \
|
||||
tests/test_log.c \
|
||||
tests/test_str_util.c \
|
||||
tests/test_platform.c
|
||||
tests/test_platform.c \
|
||||
tests/test_dump_verify.c
|
||||
|
||||
TEST_EXTRACTION_SRCS = \
|
||||
tests/test_extraction.c \
|
||||
@@ -313,7 +315,8 @@ TEST_STORE_SRCS = \
|
||||
tests/test_store_arch.c \
|
||||
tests/test_store_bulk.c \
|
||||
tests/test_store_pragmas.c \
|
||||
tests/test_store_checkpoint.c
|
||||
tests/test_store_checkpoint.c \
|
||||
tests/test_dump_verify_io.c
|
||||
|
||||
TEST_CYPHER_SRCS = \
|
||||
tests/test_cypher.c
|
||||
|
||||
@@ -445,6 +445,7 @@ codebase-memory-mcp config reset auto_index # reset to default
|
||||
| `CBM_DOWNLOAD_URL` | *(GitHub releases)* | Override the download URL for updates. Used for testing or self-hosted deployments. |
|
||||
| `CBM_LOG_LEVEL` | `info` | Set the minimum log level. Accepted values (case-insensitive): `debug`, `info`, `warn`, `error`, `none` — or their numeric equivalents `0`–`4` matching the internal enum. Logs go to stderr; stdout is reserved for MCP JSON-RPC. |
|
||||
| `CBM_WORKERS` | *(detected)* | Override the parallel-indexing worker count returned by `cbm_default_worker_count`. Useful inside containers where `sysconf(_SC_NPROCESSORS_ONLN)` reports host CPUs rather than the cgroup's effective quota. Range 1–256; invalid values are ignored with a warning. |
|
||||
| `CBM_DUMP_VERIFY_MIN_RATIO` | `0.5` | After indexing, compare persisted SQLite node count to the in-memory dump count. When persisted nodes fall below this fraction of committed nodes (and committed > 50), `index_repository` returns `status:"degraded"` instead of silent `indexed`. Range 0–1; set `0` to disable. Invalid values are ignored with a warning. |
|
||||
|
||||
```bash
|
||||
# Store indexes in a custom directory
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* dump_verify.c — Post-dump plausibility gate (#334).
|
||||
*/
|
||||
#include "foundation/dump_verify.h"
|
||||
#include "foundation/constants.h"
|
||||
#include "foundation/log.h"
|
||||
#include "foundation/platform.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
bool cbm_dump_verify_is_degraded(int committed_nodes, int persisted_nodes, double ratio,
|
||||
int min_floor) {
|
||||
if (ratio <= 0.0) {
|
||||
return false;
|
||||
}
|
||||
if (committed_nodes < 0) {
|
||||
return false;
|
||||
}
|
||||
if (committed_nodes <= min_floor) {
|
||||
return false;
|
||||
}
|
||||
if (persisted_nodes < 0) {
|
||||
return true;
|
||||
}
|
||||
return (double)persisted_nodes < (double)committed_nodes * ratio;
|
||||
}
|
||||
|
||||
double cbm_dump_verify_min_ratio(void) {
|
||||
char buf[CBM_SZ_32];
|
||||
if (cbm_safe_getenv("CBM_DUMP_VERIFY_MIN_RATIO", buf, sizeof(buf), NULL) != NULL) {
|
||||
char *end = NULL;
|
||||
double r = strtod(buf, &end);
|
||||
if (end != buf && r >= 0.0 && r <= 1.0) {
|
||||
return r;
|
||||
}
|
||||
cbm_log_warn("dump_verify.env.invalid", "value", buf, "fallback", "0.5");
|
||||
}
|
||||
return CBM_DUMP_VERIFY_DEFAULT_RATIO;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* dump_verify.h — Post-dump plausibility gate (#334 design b).
|
||||
*
|
||||
* Compares committed in-memory node counts against persisted SQLite rows
|
||||
* after index_repository completes. Nodes-only gate (edges shrink legitimately
|
||||
* at dump when endpoints fail to resolve).
|
||||
*/
|
||||
#ifndef CBM_DUMP_VERIFY_H
|
||||
#define CBM_DUMP_VERIFY_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/** Repos with at most this many committed nodes skip the ratio gate. */
|
||||
enum { CBM_DUMP_VERIFY_MIN_FLOOR = 50 };
|
||||
|
||||
/** Default minimum persisted/committed ratio when env is unset. */
|
||||
#define CBM_DUMP_VERIFY_DEFAULT_RATIO 0.5
|
||||
|
||||
/**
|
||||
* True when persisted_nodes is implausibly below committed_nodes.
|
||||
*
|
||||
* Returns false when ratio <= 0 (gate disabled), committed_nodes < 0 (no dump),
|
||||
* committed_nodes <= min_floor (sparse repo), or persisted >= committed * ratio.
|
||||
* Returns true when persisted_nodes < 0 (count error).
|
||||
*/
|
||||
bool cbm_dump_verify_is_degraded(int committed_nodes, int persisted_nodes, double ratio,
|
||||
int min_floor);
|
||||
|
||||
/** Read CBM_DUMP_VERIFY_MIN_RATIO (0..1); invalid/unset -> default 0.5. Set 0 to disable. */
|
||||
double cbm_dump_verify_min_ratio(void);
|
||||
|
||||
#endif /* CBM_DUMP_VERIFY_H */
|
||||
+70
-14
@@ -54,6 +54,7 @@ enum {
|
||||
#include "foundation/compat_thread.h"
|
||||
#include "foundation/log.h"
|
||||
#include "foundation/str_util.h"
|
||||
#include "foundation/dump_verify.h"
|
||||
#include "foundation/compat_regex.h"
|
||||
#include "pipeline/artifact.h"
|
||||
|
||||
@@ -2687,28 +2688,82 @@ static void add_excluded_summary(yyjson_mut_doc *doc, yyjson_mut_val *root, char
|
||||
yyjson_mut_obj_add_val(doc, root, "excluded", excluded);
|
||||
}
|
||||
|
||||
/* Build the success portion of the index_repository response. */
|
||||
static void build_index_success_response(cbm_mcp_server_t *srv, yyjson_mut_doc *doc,
|
||||
/* Build the success portion of the index_repository response.
|
||||
* Returns true when status should be "degraded" (#334 plausibility gate). */
|
||||
static bool build_index_success_response(cbm_mcp_server_t *srv, yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *root, const char *project_name,
|
||||
const char *repo_path, bool persistence,
|
||||
const char *repo_path, bool persistence, cbm_pipeline_t *p,
|
||||
char **excluded_dirs, int excluded_count) {
|
||||
add_excluded_summary(doc, root, excluded_dirs, excluded_count);
|
||||
|
||||
int exp_nodes = -1;
|
||||
int exp_edges = -1;
|
||||
cbm_pipeline_get_committed_counts(p, &exp_nodes, &exp_edges);
|
||||
|
||||
const double ratio = cbm_dump_verify_min_ratio();
|
||||
const int min_floor = CBM_DUMP_VERIFY_MIN_FLOOR;
|
||||
|
||||
cbm_store_t *store = resolve_store(srv, project_name);
|
||||
int nodes = 0;
|
||||
int edges = 0;
|
||||
bool degraded = false;
|
||||
|
||||
if (!store) {
|
||||
return;
|
||||
degraded = true;
|
||||
} else {
|
||||
nodes = cbm_store_count_nodes(store, project_name);
|
||||
edges = cbm_store_count_edges(store, project_name);
|
||||
if (nodes < 0) {
|
||||
degraded = true;
|
||||
nodes = 0;
|
||||
edges = edges >= 0 ? edges : 0;
|
||||
} else if (cbm_dump_verify_is_degraded(exp_nodes, nodes, ratio, min_floor)) {
|
||||
(void)cbm_store_checkpoint(store);
|
||||
int nodes2 = cbm_store_count_nodes(store, project_name);
|
||||
int edges2 = cbm_store_count_edges(store, project_name);
|
||||
if (nodes2 >= 0) {
|
||||
nodes = nodes2;
|
||||
}
|
||||
if (edges2 >= 0) {
|
||||
edges = edges2;
|
||||
}
|
||||
degraded = cbm_dump_verify_is_degraded(exp_nodes, nodes, ratio, min_floor);
|
||||
}
|
||||
}
|
||||
int nodes = cbm_store_count_nodes(store, project_name);
|
||||
int edges = cbm_store_count_edges(store, project_name);
|
||||
|
||||
yyjson_mut_obj_add_int(doc, root, "nodes", nodes);
|
||||
yyjson_mut_obj_add_int(doc, root, "edges", edges);
|
||||
if (exp_nodes >= 0) {
|
||||
yyjson_mut_obj_add_int(doc, root, "expected_nodes", exp_nodes);
|
||||
yyjson_mut_obj_add_int(doc, root, "expected_edges", exp_edges);
|
||||
}
|
||||
|
||||
if (degraded) {
|
||||
if (!store) {
|
||||
yyjson_mut_obj_add_str(doc, root, "hint",
|
||||
"Index database failed integrity check and was removed. "
|
||||
"Re-run index_repository(repo_path=...) to rebuild.");
|
||||
cbm_log_warn("dump.verify", "reason", "store_missing", "expected_nodes",
|
||||
exp_nodes >= 0 ? "set" : "unknown");
|
||||
} else {
|
||||
char exp_buf[MCP_FIELD_SIZE];
|
||||
char got_buf[MCP_FIELD_SIZE];
|
||||
snprintf(exp_buf, sizeof(exp_buf), "%d", exp_nodes);
|
||||
snprintf(got_buf, sizeof(got_buf), "%d", nodes);
|
||||
yyjson_mut_obj_add_str(
|
||||
doc, root, "hint",
|
||||
"Persisted far fewer nodes than indexed — likely durability loss from a "
|
||||
"hard-killed sibling process. Re-run index_repository(repo_path=...) to rebuild.");
|
||||
cbm_log_warn("dump.verify", "expected_nodes", exp_buf, "persisted_nodes", got_buf);
|
||||
}
|
||||
}
|
||||
|
||||
char adr_path[CBM_SZ_4K];
|
||||
snprintf(adr_path, sizeof(adr_path), "%s/.codebase-memory/adr.md", repo_path);
|
||||
struct stat adr_st;
|
||||
bool adr_exists = (stat(adr_path, &adr_st) == 0);
|
||||
yyjson_mut_obj_add_bool(doc, root, "adr_present", adr_exists);
|
||||
if (!adr_exists) {
|
||||
if (!adr_exists && !degraded) {
|
||||
yyjson_mut_obj_add_str(
|
||||
doc, root, "adr_hint",
|
||||
"Project indexed. Consider creating an Architecture Decision Record: "
|
||||
@@ -2723,6 +2778,8 @@ static void build_index_success_response(cbm_mcp_server_t *srv, yyjson_mut_doc *
|
||||
"Persistent artifact written to .codebase-memory/graph.db.zst. "
|
||||
"Commit this file to share the index with teammates.");
|
||||
}
|
||||
|
||||
return degraded;
|
||||
}
|
||||
|
||||
static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
|
||||
@@ -2803,19 +2860,18 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
|
||||
yyjson_mut_doc_set_root(doc, root);
|
||||
|
||||
yyjson_mut_obj_add_str(doc, root, "project", project_name);
|
||||
yyjson_mut_obj_add_str(doc, root, "status", rc == 0 ? "indexed" : "error");
|
||||
|
||||
if (rc != 0) {
|
||||
if (rc == 0) {
|
||||
bool degraded = build_index_success_response(srv, doc, root, project_name, repo_path,
|
||||
persistence, p, excluded_dirs, excluded_count);
|
||||
yyjson_mut_obj_add_str(doc, root, "status", degraded ? "degraded" : "indexed");
|
||||
} else {
|
||||
yyjson_mut_obj_add_str(doc, root, "status", "error");
|
||||
yyjson_mut_obj_add_str(doc, root, "hint",
|
||||
"Pipeline failed. Check repo_path exists and contains source files. "
|
||||
"Try mode='fast' for a quicker diagnostic run.");
|
||||
}
|
||||
|
||||
if (rc == 0) {
|
||||
build_index_success_response(srv, doc, root, project_name, repo_path, persistence,
|
||||
excluded_dirs, excluded_count);
|
||||
}
|
||||
|
||||
char *json = yy_doc_to_str(doc);
|
||||
yyjson_mut_doc_free(doc);
|
||||
/* Free the pipeline only after the response doc copied the excluded list. */
|
||||
|
||||
@@ -93,6 +93,10 @@ struct cbm_pipeline {
|
||||
|
||||
/* User-defined extension overrides (loaded once per run) */
|
||||
cbm_userconfig_t *userconfig;
|
||||
|
||||
/* Committed graph size at dump time (-1 = dump did not run). #334 gate axis. */
|
||||
int committed_nodes;
|
||||
int committed_edges;
|
||||
};
|
||||
|
||||
/* ── Global pkgmap (one active pipeline at a time) ─────────────── */
|
||||
@@ -154,6 +158,8 @@ cbm_pipeline_t *cbm_pipeline_new(const char *repo_path, const char *db_path,
|
||||
p->branch_qn = cbm_git_context_branch_qn(p->project_name, &p->git_ctx);
|
||||
p->mode = mode;
|
||||
p->persistence = false;
|
||||
p->committed_nodes = -1;
|
||||
p->committed_edges = -1;
|
||||
atomic_init(&p->cancelled, 0);
|
||||
|
||||
return p;
|
||||
@@ -218,6 +224,15 @@ void cbm_pipeline_get_excluded(const cbm_pipeline_t *p, char ***out, int *count)
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_pipeline_get_committed_counts(const cbm_pipeline_t *p, int *nodes, int *edges) {
|
||||
if (nodes) {
|
||||
*nodes = p ? p->committed_nodes : -1;
|
||||
}
|
||||
if (edges) {
|
||||
*edges = p ? p->committed_edges : -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Resolve the DB path for this pipeline. Caller must free(). */
|
||||
static char *resolve_db_path(const cbm_pipeline_t *p) {
|
||||
char *path = malloc(CBM_SZ_1K);
|
||||
@@ -837,6 +852,8 @@ static int dump_and_persist_hashes(cbm_pipeline_t *p, const cbm_file_info_t *fil
|
||||
cbm_log_error("pipeline.err", "phase", "dump");
|
||||
return rc;
|
||||
}
|
||||
p->committed_nodes = cbm_gbuf_node_count(p->gbuf);
|
||||
p->committed_edges = cbm_gbuf_edge_count(p->gbuf);
|
||||
cbm_log_info("pass.timing", "pass", "dump", "elapsed_ms", itoa_buf((int)elapsed_ms(*t)));
|
||||
cbm_store_t *hash_store = cbm_store_open_path(db_path);
|
||||
if (hash_store) {
|
||||
|
||||
@@ -72,6 +72,10 @@ int cbm_pipeline_get_mode(const cbm_pipeline_t *p);
|
||||
* to NULL/0 when p is NULL or nothing was excluded. Do not free. */
|
||||
void cbm_pipeline_get_excluded(const cbm_pipeline_t *p, char ***out, int *count);
|
||||
|
||||
/* Committed node/edge counts captured at dump time (-1 when dump did not run).
|
||||
* Nodes are the #334 plausibility-gate axis; edges are informational only. */
|
||||
void cbm_pipeline_get_committed_counts(const cbm_pipeline_t *p, int *nodes, int *edges);
|
||||
|
||||
/* ── Index lock (prevents concurrent pipeline runs on same DB) ──── */
|
||||
|
||||
/* Try to acquire the global index lock. Returns true if acquired,
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* test_dump_verify.c — Post-dump plausibility gate (#334).
|
||||
*
|
||||
* Pure-function matrix mirrors sast-ai-app checkSilentDegradation cases.
|
||||
* I/O-level coverage that drives the gate against a real on-disk SQLite store
|
||||
* lives in test_dump_verify_io.c (store-linked, excluded from test-foundation).
|
||||
*/
|
||||
#include "../src/foundation/compat.h"
|
||||
#include "../src/foundation/dump_verify.h"
|
||||
#include "test_framework.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
TEST(dump_verify_no_baseline) {
|
||||
ASSERT_FALSE(cbm_dump_verify_is_degraded(-1, 500, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(dump_verify_sparse_at_floor) {
|
||||
ASSERT_FALSE(cbm_dump_verify_is_degraded(50, 10, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
|
||||
ASSERT_FALSE(cbm_dump_verify_is_degraded(12, 5, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(dump_verify_shortfall_below_ratio) {
|
||||
ASSERT_TRUE(cbm_dump_verify_is_degraded(1000, 400, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(dump_verify_just_above_ratio) {
|
||||
ASSERT_FALSE(cbm_dump_verify_is_degraded(1000, 500, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(dump_verify_just_below_ratio) {
|
||||
ASSERT_TRUE(cbm_dump_verify_is_degraded(1000, 499, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(dump_verify_zero_persisted) {
|
||||
ASSERT_TRUE(cbm_dump_verify_is_degraded(1000, 0, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(dump_verify_growth) {
|
||||
ASSERT_FALSE(cbm_dump_verify_is_degraded(500, 750, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(dump_verify_count_error) {
|
||||
ASSERT_TRUE(cbm_dump_verify_is_degraded(1000, -1, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(dump_verify_ratio_zero_disables) {
|
||||
ASSERT_FALSE(cbm_dump_verify_is_degraded(1000, 10, 0.0, CBM_DUMP_VERIFY_MIN_FLOOR));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(dump_verify_loosened_ratio) {
|
||||
ASSERT_FALSE(cbm_dump_verify_is_degraded(1000, 600, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(dump_verify_tightened_ratio) {
|
||||
ASSERT_TRUE(cbm_dump_verify_is_degraded(1000, 900, 0.95, CBM_DUMP_VERIFY_MIN_FLOOR));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(dump_verify_edges_shrank_nodes_ok) {
|
||||
/* Edges are not gated; this documents nodes-only semantics for integrators. */
|
||||
ASSERT_FALSE(cbm_dump_verify_is_degraded(200, 200, 0.5, CBM_DUMP_VERIFY_MIN_FLOOR));
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(dump_verify) {
|
||||
RUN_TEST(dump_verify_no_baseline);
|
||||
RUN_TEST(dump_verify_sparse_at_floor);
|
||||
RUN_TEST(dump_verify_shortfall_below_ratio);
|
||||
RUN_TEST(dump_verify_just_above_ratio);
|
||||
RUN_TEST(dump_verify_just_below_ratio);
|
||||
RUN_TEST(dump_verify_zero_persisted);
|
||||
RUN_TEST(dump_verify_growth);
|
||||
RUN_TEST(dump_verify_count_error);
|
||||
RUN_TEST(dump_verify_ratio_zero_disables);
|
||||
RUN_TEST(dump_verify_loosened_ratio);
|
||||
RUN_TEST(dump_verify_tightened_ratio);
|
||||
RUN_TEST(dump_verify_edges_shrank_nodes_ok);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* test_dump_verify_io.c — I/O-level coverage for the #334 plausibility gate.
|
||||
*
|
||||
* Drives cbm_dump_verify_is_degraded against a real on-disk SQLite store and
|
||||
* the actual cbm_store_count_nodes / cbm_store_open_path_query path used by
|
||||
* resolve_store(). Includes a fork/crash port of bulk_crash_recovery proving
|
||||
* uncommitted WAL frames are discarded on the next open and the gate trips when
|
||||
* persisted rows fall short of the committed dump intent.
|
||||
*
|
||||
* Store-linked (excluded from the fast test-foundation target); runs under the
|
||||
* full test-runner. The pure-function matrix lives in test_dump_verify.c.
|
||||
*/
|
||||
#include "../src/foundation/compat.h"
|
||||
#include "../src/foundation/dump_verify.h"
|
||||
#include "test_framework.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <store/store.h>
|
||||
#ifndef _WIN32
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#define DV_PROJECT "io_dump_verify"
|
||||
|
||||
static void dv_temp_path(char *buf, size_t n) {
|
||||
snprintf(buf, n, "%s/cmm_dump_verify_%d.db", cbm_tmpdir(), (int)getpid());
|
||||
}
|
||||
|
||||
static void dv_cleanup(const char *path) {
|
||||
remove(path);
|
||||
char aux[640];
|
||||
snprintf(aux, sizeof(aux), "%s-wal", path);
|
||||
remove(aux);
|
||||
snprintf(aux, sizeof(aux), "%s-shm", path);
|
||||
remove(aux);
|
||||
}
|
||||
|
||||
/* Commit `count` distinct nodes to the on-disk store at `path`. */
|
||||
static int dv_write_nodes(const char *path, int count) {
|
||||
cbm_store_t *s = cbm_store_open_path(path);
|
||||
if (!s)
|
||||
return -1;
|
||||
if (cbm_store_upsert_project(s, DV_PROJECT, "/tmp/io_dump_verify") != CBM_STORE_OK) {
|
||||
cbm_store_close(s);
|
||||
return -1;
|
||||
}
|
||||
cbm_store_begin(s);
|
||||
for (int i = 0; i < count; i++) {
|
||||
char qn[64];
|
||||
snprintf(qn, sizeof(qn), "io.node.%d", i);
|
||||
cbm_node_t node = {.project = DV_PROJECT,
|
||||
.label = "Function",
|
||||
.name = "n",
|
||||
.qualified_name = qn,
|
||||
.file_path = "io.c",
|
||||
.start_line = i,
|
||||
.end_line = i};
|
||||
if (cbm_store_upsert_node(s, &node) < 0) {
|
||||
cbm_store_close(s);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
cbm_store_commit(s);
|
||||
cbm_store_close(s);
|
||||
return count;
|
||||
}
|
||||
|
||||
/* Persisted node count via the same query path resolve_store() uses. */
|
||||
static int dv_persisted_nodes(const char *path) {
|
||||
cbm_store_t *s = cbm_store_open_path_query(path);
|
||||
if (!s)
|
||||
return CBM_STORE_ERR;
|
||||
int n = cbm_store_count_nodes(s, DV_PROJECT);
|
||||
cbm_store_close(s);
|
||||
return n;
|
||||
}
|
||||
|
||||
TEST(dump_verify_io_full_persist_ok) {
|
||||
char path[640];
|
||||
dv_temp_path(path, sizeof(path));
|
||||
dv_cleanup(path);
|
||||
|
||||
ASSERT_EQ(dv_write_nodes(path, 200), 200);
|
||||
int persisted = dv_persisted_nodes(path);
|
||||
ASSERT_EQ(persisted, 200);
|
||||
/* committed == persisted: a faithful dump is never degraded. */
|
||||
ASSERT_FALSE(cbm_dump_verify_is_degraded(200, persisted, CBM_DUMP_VERIFY_DEFAULT_RATIO,
|
||||
CBM_DUMP_VERIFY_MIN_FLOOR));
|
||||
|
||||
dv_cleanup(path);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(dump_verify_io_shortfall_after_delete) {
|
||||
char path[640];
|
||||
dv_temp_path(path, sizeof(path));
|
||||
dv_cleanup(path);
|
||||
|
||||
ASSERT_EQ(dv_write_nodes(path, 1000), 1000);
|
||||
ASSERT_FALSE(cbm_dump_verify_is_degraded(1000, dv_persisted_nodes(path),
|
||||
CBM_DUMP_VERIFY_DEFAULT_RATIO,
|
||||
CBM_DUMP_VERIFY_MIN_FLOOR));
|
||||
|
||||
/* Simulate persisted loss: most rows vanish while the committed intent
|
||||
* (1000) is unchanged — the #334 silent-degradation signature. */
|
||||
cbm_store_t *s = cbm_store_open_path(path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
ASSERT_EQ(cbm_store_exec(s, "DELETE FROM nodes WHERE start_line >= 100"), CBM_STORE_OK);
|
||||
cbm_store_close(s);
|
||||
|
||||
int persisted = dv_persisted_nodes(path);
|
||||
ASSERT_EQ(persisted, 100);
|
||||
ASSERT_TRUE(cbm_dump_verify_is_degraded(1000, persisted, CBM_DUMP_VERIFY_DEFAULT_RATIO,
|
||||
CBM_DUMP_VERIFY_MIN_FLOOR));
|
||||
|
||||
dv_cleanup(path);
|
||||
PASS();
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
TEST(dump_verify_io_fork_crash_uncommitted_discarded) {
|
||||
char path[640];
|
||||
dv_temp_path(path, sizeof(path));
|
||||
dv_cleanup(path);
|
||||
|
||||
/* Baseline above the sparse floor is durably committed before the crash. */
|
||||
ASSERT_EQ(dv_write_nodes(path, 60), 60);
|
||||
|
||||
pid_t pid = fork();
|
||||
ASSERT_TRUE(pid >= 0);
|
||||
if (pid == 0) {
|
||||
/* Child: write a large uncommitted batch then crash without commit. */
|
||||
cbm_store_t *s = cbm_store_open_path(path);
|
||||
if (!s)
|
||||
_exit(2);
|
||||
cbm_store_begin(s);
|
||||
for (int i = 1000; i < 6000; i++) {
|
||||
char qn[64];
|
||||
snprintf(qn, sizeof(qn), "io.node.%d", i);
|
||||
cbm_node_t node = {.project = DV_PROJECT,
|
||||
.label = "Function",
|
||||
.name = "n",
|
||||
.qualified_name = qn,
|
||||
.file_path = "io.c",
|
||||
.start_line = i,
|
||||
.end_line = i};
|
||||
cbm_store_upsert_node(s, &node);
|
||||
}
|
||||
/* No commit, no clean close: simulate a crash mid-dump. */
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
ASSERT_TRUE(waitpid(pid, &status, 0) == pid);
|
||||
|
||||
/* WAL recovery discards the child's uncommitted frames: only the baseline
|
||||
* survives, so persisted (60) falls far short of the dump intent (5060). */
|
||||
int persisted = dv_persisted_nodes(path);
|
||||
ASSERT_EQ(persisted, 60);
|
||||
ASSERT_TRUE(cbm_dump_verify_is_degraded(5060, persisted, CBM_DUMP_VERIFY_DEFAULT_RATIO,
|
||||
CBM_DUMP_VERIFY_MIN_FLOOR));
|
||||
|
||||
dv_cleanup(path);
|
||||
PASS();
|
||||
}
|
||||
#endif /* _WIN32 */
|
||||
|
||||
SUITE(dump_verify_io) {
|
||||
RUN_TEST(dump_verify_io_full_persist_ok);
|
||||
RUN_TEST(dump_verify_io_shortfall_after_delete);
|
||||
#ifndef _WIN32
|
||||
RUN_TEST(dump_verify_io_fork_crash_uncommitted_discarded);
|
||||
#endif
|
||||
}
|
||||
@@ -97,6 +97,8 @@ extern void suite_grammar_probe_g(void);
|
||||
extern void suite_incremental(void);
|
||||
extern void suite_simhash(void);
|
||||
extern void suite_stack_overflow(void);
|
||||
extern void suite_dump_verify(void);
|
||||
extern void suite_dump_verify_io(void);
|
||||
|
||||
/* Free the main thread's thread-local node-type bitset cache before exit so
|
||||
* LeakSanitizer (Linux x64) doesn't report it. Worker threads free their own
|
||||
@@ -114,6 +116,7 @@ int main(void) {
|
||||
RUN_SUITE(log);
|
||||
RUN_SUITE(str_util);
|
||||
RUN_SUITE(platform);
|
||||
RUN_SUITE(dump_verify);
|
||||
|
||||
/* Existing C code regression tests */
|
||||
RUN_SUITE(ac);
|
||||
@@ -131,6 +134,7 @@ int main(void) {
|
||||
RUN_SUITE(store_bulk);
|
||||
RUN_SUITE(store_pragmas);
|
||||
RUN_SUITE(store_checkpoint);
|
||||
RUN_SUITE(dump_verify_io);
|
||||
|
||||
/* Cypher (M6) */
|
||||
RUN_SUITE(cypher);
|
||||
|
||||
Reference in new issue
Block a user