feat(store): recover overlay compaction claims

Add a default-inert recovery primitive for overlay generations left in compacting after process restart or confirmed worker shutdown. The API requeues compacting generations to overlay_ready in one existing BEGIN IMMEDIATE transaction and returns the number recovered, with no timeout heuristic or live-worker assumption.

Cover the recovery path in store_nodes by claiming a ready generation, recovering it, proving failed generations are untouched, and proving the recovered generation can be claimed again. The header documents that callers must not run recovery while a compactor is active.

Validation: make -f Makefile.cbm -j8 build/c/test-runner; CBM_ONLY_SUITE=store_nodes build/c/test-runner; git diff --check; bash scripts/check-source-safety.sh src/store/store.c src/store/store.h tests/test_store_nodes.c; make -f Makefile.cbm -j8 cbm.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-07-04 03:43:06 -04:00
parent 66703ad9eb
commit 3439153ec4
3 changed files with 112 additions and 0 deletions
+50
View File
@@ -4076,6 +4076,56 @@ int cbm_store_claim_ready_overlay_generation(cbm_store_t *s, const char *project
return CBM_STORE_OK;
}
int cbm_store_recover_compacting_overlay_generations(cbm_store_t *s, const char *project,
int *out_recovered) {
if (out_recovered) {
*out_recovered = 0;
}
if (!s || !s->db || !project || !project[0] || !out_recovered) {
if (s) {
store_set_error(s, "recover_compacting_overlay_generations: invalid argument");
}
return CBM_STORE_ERR;
}
int rc = cbm_store_begin(s);
if (rc != CBM_STORE_OK) {
return rc;
}
sqlite3_stmt *stmt = NULL;
static const char sql[] =
"UPDATE overlay_generations SET status = ?3, updated_at = ?4 "
"WHERE project = ?1 AND status = ?2;";
if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
store_set_error_sqlite(s, "recover_compacting_overlay_generations prepare");
(void)cbm_store_rollback(s);
return CBM_STORE_ERR;
}
char ts[CBM_SZ_64];
iso_now(ts, sizeof(ts));
bind_text(stmt, ST_COL_1, project);
bind_text(stmt, ST_COL_2, CBM_STORE_OVERLAY_STATUS_COMPACTING);
bind_text(stmt, ST_COL_3, CBM_STORE_OVERLAY_STATUS_READY);
bind_text(stmt, ST_COL_4, ts);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
store_set_error_sqlite(s, "recover_compacting_overlay_generations");
(void)cbm_store_rollback(s);
return CBM_STORE_ERR;
}
int recovered = sqlite3_changes(s->db);
rc = cbm_store_commit(s);
if (rc != CBM_STORE_OK) {
(void)cbm_store_rollback(s);
return rc;
}
*out_recovered = recovered;
return CBM_STORE_OK;
}
static int store_resolve_node_id(cbm_store_t *s, const char *project, const char *qn,
int64_t *out_id) {
const char *qns[1] = {qn};
+4
View File
@@ -694,6 +694,10 @@ int cbm_store_count_overlay_generations(cbm_store_t *s, const char *project,
int cbm_store_claim_ready_overlay_generation(cbm_store_t *s, const char *project,
int64_t *out_overlay_generation,
int64_t *out_base_generation);
/* Requeue compacting overlay generations after process restart or confirmed
* worker shutdown. Callers must not run this while a compactor is active. */
int cbm_store_recover_compacting_overlay_generations(cbm_store_t *s, const char *project,
int *out_recovered);
/* Publish one file's replacement facts into overlay storage. This does not
* mutate canonical nodes/edges; active read paths decide later how to combine
+58
View File
@@ -1373,6 +1373,63 @@ TEST(store_claim_ready_overlay_generation_ignores_nonready_and_validates_outputs
PASS();
}
TEST(store_recover_compacting_overlay_generations_requeues_abandoned_claims) {
enum { BASE_GENERATION = 5, SENTINEL = 42 };
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
ASSERT_EQ(cbm_store_upsert_project(s, "test", "/tmp/test"), CBM_STORE_OK);
int64_t compacting = 0;
int64_t failed = 0;
ASSERT_EQ(cbm_store_reserve_overlay_generation(s, "test", BASE_GENERATION,
&compacting),
CBM_STORE_OK);
ASSERT_EQ(cbm_store_reserve_overlay_generation(s, "test", BASE_GENERATION, &failed),
CBM_STORE_OK);
ASSERT_EQ(cbm_store_set_overlay_generation_status(s, "test", compacting,
CBM_STORE_OVERLAY_STATUS_READY),
CBM_STORE_OK);
ASSERT_EQ(cbm_store_set_overlay_generation_status(s, "test", failed,
CBM_STORE_OVERLAY_STATUS_FAILED),
CBM_STORE_OK);
int64_t claimed = 0;
int64_t base = 0;
ASSERT_EQ(cbm_store_claim_ready_overlay_generation(s, "test", &claimed, &base),
CBM_STORE_OK);
ASSERT_EQ(claimed, compacting);
ASSERT_EQ(base, BASE_GENERATION);
int recovered = SENTINEL;
ASSERT_EQ(cbm_store_recover_compacting_overlay_generations(s, "test", &recovered),
CBM_STORE_OK);
ASSERT_EQ(recovered, 1);
ASSERT_EQ(store_count_overlay_generation_row(s, "test", compacting, BASE_GENERATION,
CBM_STORE_OVERLAY_STATUS_READY),
1);
ASSERT_EQ(store_count_overlay_generation_row(s, "test", failed, BASE_GENERATION,
CBM_STORE_OVERLAY_STATUS_FAILED),
1);
ASSERT_EQ(cbm_store_claim_ready_overlay_generation(s, "test", &claimed, &base),
CBM_STORE_OK);
ASSERT_EQ(claimed, compacting);
recovered = SENTINEL;
ASSERT_EQ(cbm_store_recover_compacting_overlay_generations(s, "test", &recovered),
CBM_STORE_OK);
ASSERT_EQ(recovered, 1);
recovered = SENTINEL;
ASSERT_EQ(cbm_store_recover_compacting_overlay_generations(s, "missing", &recovered),
CBM_STORE_OK);
ASSERT_EQ(recovered, 0);
ASSERT_EQ(cbm_store_recover_compacting_overlay_generations(s, "test", NULL),
CBM_STORE_ERR);
cbm_store_close(s);
PASS();
}
TEST(store_overlay_file_delta_publish_rows_and_tombstone) {
enum { BASE_GENERATION = 3 };
cbm_store_t *s = cbm_store_open_memory();
@@ -5345,6 +5402,7 @@ SUITE(store_nodes) {
RUN_TEST(store_overlay_generation_rejects_invalid_inputs);
RUN_TEST(store_claim_ready_overlay_generation_claims_oldest_once);
RUN_TEST(store_claim_ready_overlay_generation_ignores_nonready_and_validates_outputs);
RUN_TEST(store_recover_compacting_overlay_generations_requeues_abandoned_claims);
RUN_TEST(store_overlay_file_delta_publish_rows_and_tombstone);
RUN_TEST(store_delete_project_clears_overlay_fts);
RUN_TEST(store_overlay_file_delta_publish_rejects_invalid_delta_without_rows);