fix: guard slow incremental reindex routing

Add a config-backed incremental_reindex policy with default fast so full and moderate indexes rebuild atomically instead of entering the current disk incremental path, which still performs full-graph quality work and can be slower than a clean rebuild. Keep always for benchmarking and off for deterministic full rebuilds.

Harden the existing incremental path around allocation failures before mutation: classify, changed-file list, graph buffer creation, changed-path set, and deleted-path copies now fail closed or preserve existing nodes rather than dereferencing NULL or recording corrupt path arrays.

Reuse the existing pipeline nanosecond constant for file-hash mtimes and keep the dedicated incremental suite opted into incremental_reindex=always so its canaries continue exercising the legacy disk incremental implementation.

Validation: make -f Makefile.cbm build/c/test-runner cbm; CBM_ONLY_SUITE=pipeline build/c/test-runner: 212 passed; CBM_ONLY_SUITE=cli build/c/test-runner: 102 passed; CBM_ONLY_SUITE=mcp build/c/test-runner: 113 passed; CBM_ONLY_SUITE=tool_consolidation build/c/test-runner: 94 passed; git diff --check.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-06-28 12:14:52 -04:00
parent 9105f0f73e
commit a7e80d8a61
6 changed files with 114 additions and 15 deletions
+6
View File
@@ -2706,6 +2706,12 @@ const cbm_config_entry_t CBM_CONFIG_REGISTRY[] = {
"Re-index if DB is older than N seconds (0=disabled)",
"0-2592000",
"0=disabled. 3600=hourly, 86400=daily, 604800=weekly. Runs on startup if stale."},
{"incremental_reindex", "fast", NULL, "Indexing",
"When to use the disk incremental reindex path",
"fast|always|off",
"'fast' uses incremental only for fast-mode indexes. Full/moderate modes rebuild atomically because "
"their current incremental path still performs full-graph quality work. 'always' preserves the legacy "
"route for benchmarking; 'off' always rebuilds from scratch."},
/* ── Search ── */
{"search_limit", "50", NULL, "Search",
"Default max results for search_graph/search_code",
+30 -4
View File
@@ -13,7 +13,6 @@
#include "foundation/constants.h"
enum { CBM_DIR_PERMS = 0755, PL_RING = 4, PL_RING_MASK = 3, PL_SEQ_PASSES = 6 };
#define PL_NSEC_PER_SEC 1000000000LL
#include "cli/cli.h"
#include "pipeline/pipeline.h"
#include "pipeline/artifact.h"
@@ -63,6 +62,12 @@ bool cbm_pipeline_try_lock(void) {
#define LOCK_SPIN_NS 100000000 /* 100ms between lock retries */
typedef enum {
CBM_INCREMENTAL_REINDEX_FAST = 0,
CBM_INCREMENTAL_REINDEX_ALWAYS,
CBM_INCREMENTAL_REINDEX_OFF,
} cbm_incremental_reindex_policy_t;
void cbm_pipeline_lock(void) {
while (atomic_exchange(&g_pipeline_busy, 1) != 0) {
struct timespec ts = {0, LOCK_SPIN_NS};
@@ -88,6 +93,7 @@ struct cbm_pipeline {
double semantic_threshold;
double githistory_min_coupling;
double lsp_confidence_floor;
cbm_incremental_reindex_policy_t incremental_reindex;
atomic_int cancelled;
cbm_store_t *flush_store; /* when set, use flush_to_store instead of dump_to_sqlite */
bool persistence; /* write .codebase-memory/graph.db.zst after indexing */
@@ -173,6 +179,7 @@ cbm_pipeline_t *cbm_pipeline_new(const char *repo_path, const char *db_path,
p->semantic_threshold = 0.0;
p->githistory_min_coupling = 0.0;
p->lsp_confidence_floor = 0.0;
p->incremental_reindex = CBM_INCREMENTAL_REINDEX_FAST;
p->persistence = false;
p->committed_nodes = -1;
p->committed_edges = -1;
@@ -259,6 +266,15 @@ void cbm_pipeline_apply_config(cbm_pipeline_t *p, cbm_config_t *cfg) {
if (lsp_floor > 0.0) {
cbm_pipeline_set_lsp_confidence_floor(p, lsp_floor);
}
const char *incremental = cbm_config_get(cfg, CBM_CONFIG_INCREMENTAL_REINDEX, "fast");
if (incremental && strcmp(incremental, "always") == 0) {
p->incremental_reindex = CBM_INCREMENTAL_REINDEX_ALWAYS;
} else if (incremental && strcmp(incremental, "off") == 0) {
p->incremental_reindex = CBM_INCREMENTAL_REINDEX_OFF;
} else {
p->incremental_reindex = CBM_INCREMENTAL_REINDEX_FAST;
}
}
double cbm_pipeline_httplink_min_confidence(const cbm_pipeline_t *p) {
@@ -914,6 +930,16 @@ static int try_incremental_or_reindex(cbm_pipeline_t *p, cbm_file_info_t *files,
free(db_path);
return CBM_NOT_FOUND;
}
if (p->incremental_reindex == CBM_INCREMENTAL_REINDEX_OFF ||
(p->incremental_reindex == CBM_INCREMENTAL_REINDEX_FAST &&
p->mode != CBM_MODE_FAST)) {
cbm_log_info("pipeline.route", "path", "full", "reason",
p->incremental_reindex == CBM_INCREMENTAL_REINDEX_OFF
? "incremental_reindex=off"
: "incremental_reindex=fast_requires_fast_mode");
free(db_path);
return CBM_NOT_FOUND;
}
cbm_store_t *check_store = cbm_store_open_path(db_path);
if (check_store && cbm_store_check_integrity(check_store)) {
cbm_file_hash_t *hashes = NULL;
@@ -1248,12 +1274,12 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
if (stat(files[i].path, &fst) == 0) {
int64_t mtime_ns;
#ifdef __APPLE__
mtime_ns = ((int64_t)fst.st_mtimespec.tv_sec * 1000000000LL) +
mtime_ns = ((int64_t)fst.st_mtimespec.tv_sec * CBM_NS_PER_SEC) +
(int64_t)fst.st_mtimespec.tv_nsec;
#elif defined(_WIN32)
mtime_ns = (int64_t)fst.st_mtime * 1000000000LL;
mtime_ns = (int64_t)fst.st_mtime * CBM_NS_PER_SEC;
#else
mtime_ns = ((int64_t)fst.st_mtim.tv_sec * 1000000000LL) +
mtime_ns = ((int64_t)fst.st_mtim.tv_sec * CBM_NS_PER_SEC) +
(int64_t)fst.st_mtim.tv_nsec;
#endif
cbm_store_upsert_file_hash(hash_store, p->project_name,
+1
View File
@@ -84,6 +84,7 @@ void cbm_pipeline_set_flush_store(cbm_pipeline_t *p, cbm_store_t *store);
#define CBM_CONFIG_SEMANTIC_THRESHOLD "semantic_threshold"
#define CBM_CONFIG_GITHISTORY_MIN_COUPLING "githistory_min_coupling"
#define CBM_CONFIG_LSP_CONFIDENCE_FLOOR "lsp_confidence_floor"
#define CBM_CONFIG_INCREMENTAL_REINDEX "incremental_reindex"
/* Set the Jaccard similarity threshold for SIMILAR-edge creation (pass_similarity).
* <=0 (or unset) uses the CBM_MINHASH_JACCARD_THRESHOLD default. Before run(). */
+55 -11
View File
@@ -37,7 +37,6 @@ enum { INCR_RING_BUF = 4, INCR_RING_MASK = 3, INCR_TS_BUF = 24 };
#define CBM_MS_PER_SEC 1000.0
#define CBM_NS_PER_MS 1000000.0
#define CBM_NS_PER_SEC 1000000000LL
/* ── Timing helper (same as pipeline.c) ──────────────────────────── */
@@ -184,6 +183,10 @@ static int find_deleted_files(const char *repo_path, cbm_file_info_t *files, int
}
CBMHashTable *current = cbm_ht_create((size_t)file_count * PAIR_LEN);
if (!current) {
cbm_log_error("incremental.err", "msg", "find_deleted_files_current_oom");
return 0;
}
for (int i = 0; i < file_count; i++) {
cbm_ht_set(current, files[i].rel_path, &files[i]);
}
@@ -278,7 +281,13 @@ static int find_deleted_files(const char *repo_path, cbm_file_info_t *files, int
}
deleted = tmp;
}
deleted[del_count++] = strdup(stored[i].rel_path);
char *rp = strdup(stored[i].rel_path);
if (!rp) {
cbm_log_error("incremental.err", "msg", "find_deleted_files_strdup_oom", "rel_path",
stored[i].rel_path);
break;
}
deleted[del_count++] = rp;
}
cbm_ht_free(current);
@@ -300,6 +309,16 @@ static void free_mode_skipped(cbm_file_hash_t *ms, int count) {
free(ms);
}
static void free_deleted_paths(char **deleted, int count) {
if (!deleted) {
return;
}
for (int i = 0; i < count; i++) {
free(deleted[i]);
}
free(deleted);
}
/* ── Inbound cross-file edge preservation (incremental correctness) ──
*
* The purge step (cbm_gbuf_delete_by_file) removes a changed file's nodes,
@@ -702,6 +721,12 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
int n_unchanged = 0;
bool *is_changed =
classify_files(files, file_count, stored, stored_count, &n_changed, &n_unchanged);
if (!is_changed) {
cbm_log_error("incremental.err", "msg", "classify_files_oom");
cbm_store_free_file_hashes(stored, stored_count);
cbm_store_close(store);
return CBM_NOT_FOUND;
}
/* Classify stored files absent from current discovery: truly-deleted
* (purge) vs mode-skipped (preserve nodes AND hash rows). */
@@ -722,7 +747,7 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
if (n_changed == 0 && deleted_count == 0) {
cbm_log_info("incremental.noop", "reason", "no_changes");
free(is_changed);
free(deleted);
free_deleted_paths(deleted, deleted_count);
free_mode_skipped(mode_skipped, mode_skipped_count);
cbm_store_free_file_hashes(stored, stored_count);
cbm_store_close(store);
@@ -734,6 +759,14 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
/* Build list of changed files */
cbm_file_info_t *changed_files =
(n_changed > 0) ? malloc((size_t)n_changed * sizeof(cbm_file_info_t)) : NULL;
if (n_changed > 0 && !changed_files) {
cbm_log_error("incremental.err", "msg", "changed_files_oom");
free(is_changed);
free_deleted_paths(deleted, deleted_count);
free_mode_skipped(mode_skipped, mode_skipped_count);
cbm_store_close(store);
return CBM_NOT_FOUND;
}
int ci = 0;
for (int i = 0; i < file_count; i++) {
if (is_changed[i]) {
@@ -749,6 +782,14 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
/* Step 1: Load existing graph into RAM */
cbm_clock_gettime(CLOCK_MONOTONIC, &t);
cbm_gbuf_t *existing = cbm_gbuf_new(project, cbm_pipeline_repo_path(p));
if (!existing) {
cbm_log_error("incremental.err", "msg", "gbuf_new_oom");
free(changed_files);
free_deleted_paths(deleted, deleted_count);
free_mode_skipped(mode_skipped, mode_skipped_count);
cbm_store_close(store);
return CBM_NOT_FOUND;
}
int load_rc = cbm_gbuf_load_from_db(existing, db_path, project);
cbm_log_info("incremental.load_db", "rc", itoa_buf_incr(load_rc), "nodes",
itoa_buf_incr(cbm_gbuf_node_count(existing)), "edges",
@@ -759,10 +800,7 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
cbm_log_error("incremental.err", "msg", "load_db_failed");
cbm_gbuf_free(existing);
free(changed_files);
for (int i = 0; i < deleted_count; i++) {
free(deleted[i]);
}
free(deleted);
free_deleted_paths(deleted, deleted_count);
free_mode_skipped(mode_skipped, mode_skipped_count);
cbm_store_close(store);
return CBM_NOT_FOUND;
@@ -777,6 +815,15 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
edge_cap.gbuf = existing;
{
CBMHashTable *changed_paths = cbm_ht_create(ci > 0 ? (size_t)ci * PAIR_LEN : CBM_SZ_64);
if (!changed_paths) {
cbm_log_error("incremental.err", "msg", "changed_paths_oom");
incr_free_edge_capture(&edge_cap);
cbm_gbuf_free(existing);
free(changed_files);
free_deleted_paths(deleted, deleted_count);
free_mode_skipped(mode_skipped, mode_skipped_count);
return CBM_NOT_FOUND;
}
for (int i = 0; i < ci; i++) {
cbm_ht_set(changed_paths, changed_files[i].rel_path, &changed_files[i]);
}
@@ -812,10 +859,7 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
}
}
}
for (int i = 0; i < deleted_count; i++) {
free(deleted[i]);
}
free(deleted);
free_deleted_paths(deleted, deleted_count);
cbm_log_info("incremental.purge", "elapsed_ms", itoa_buf_incr((int)elapsed_ms_incr(t)));
/* Step 3-5: Registry + extract + resolve */
+12
View File
@@ -13,6 +13,7 @@
#include "../src/foundation/compat.h"
#include "test_framework.h"
#include "test_helpers.h"
#include <cli/cli.h>
#include <mcp/mcp.h>
#include <store/store.h>
#include <pipeline/pipeline.h>
@@ -36,6 +37,7 @@ static char g_tmpdir[256];
static char g_repodir[512];
static char g_dbpath[512];
static cbm_mcp_server_t *g_srv = NULL;
static cbm_config_t *g_cfg = NULL;
static char *g_project = NULL;
/* Baseline counts after full index */
@@ -249,6 +251,14 @@ static int incremental_setup(void) {
g_srv = cbm_mcp_server_new(NULL);
if (!g_srv)
return -1;
g_cfg = cbm_config_open(cdir);
if (!g_cfg) {
cbm_mcp_server_free(g_srv);
g_srv = NULL;
return -1;
}
cbm_config_set(g_cfg, CBM_CONFIG_INCREMENTAL_REINDEX, "always");
cbm_mcp_server_set_config(g_srv, g_cfg);
g_rss_before_full = cbm_mem_rss();
@@ -260,6 +270,8 @@ static void incremental_teardown(void) {
cbm_mcp_server_free(g_srv);
g_srv = NULL;
}
cbm_config_close(g_cfg);
g_cfg = NULL;
if (g_project) {
unlink(g_dbpath);
char wal[520], shm[520];
+10
View File
@@ -5665,6 +5665,15 @@ TEST(config_registry_includes_mcp_timeout_knobs) {
PASS();
}
TEST(config_registry_includes_incremental_reindex_policy) {
const cbm_config_entry_t *entry = find_config_entry(CBM_CONFIG_INCREMENTAL_REINDEX);
ASSERT_NOT_NULL(entry);
ASSERT_STR_EQ(entry->default_val, "fast");
ASSERT_STR_EQ(entry->category, "Indexing");
ASSERT_STR_EQ(entry->range, "fast|always|off");
PASS();
}
TEST(trackable_source_files) {
/* Common source extensions are trackable */
ASSERT_TRUE(cbm_is_trackable_file("main.go"));
@@ -6187,6 +6196,7 @@ SUITE(pipeline) {
RUN_TEST(pipeline_unit_threshold_setters_clamp_invalid_values);
RUN_TEST(pipeline_apply_config_sets_all_thresholds);
RUN_TEST(config_registry_includes_mcp_timeout_knobs);
RUN_TEST(config_registry_includes_incremental_reindex_policy);
/* File persistence */
RUN_TEST(store_file_persistence);
RUN_TEST(store_bulk_persistence);