feat(store): claim overlay compactions

Add cbm_store_claim_ready_overlay_generation() as the narrow store lifecycle primitive for a future background compactor. The claim runs in the existing BEGIN IMMEDIATE transaction path, selects the oldest ready overlay generation, and moves it from overlay_ready to compacting with a guarded status update so two callers cannot claim the same generation.

Cover oldest-ready ordering, non-ready filtering, invalid output handling, and claim-plus-compact composition in store_nodes. This keeps the change default-inert: no background thread, no default-on policy, and no shared store-handle concurrency are introduced.

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:38:16 -04:00
parent 8c0c63a37a
commit 66703ad9eb
3 changed files with 191 additions and 0 deletions
+91
View File
@@ -3985,6 +3985,97 @@ int cbm_store_count_overlay_generations(cbm_store_t *s, const char *project,
return CBM_STORE_ERR;
}
int cbm_store_claim_ready_overlay_generation(cbm_store_t *s, const char *project,
int64_t *out_overlay_generation,
int64_t *out_base_generation) {
if (out_overlay_generation) {
*out_overlay_generation = 0;
}
if (out_base_generation) {
*out_base_generation = 0;
}
if (!s || !s->db || !project || !project[0] || !out_overlay_generation ||
!out_base_generation) {
if (s) {
store_set_error(s, "claim_ready_overlay_generation: 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 select_sql[] =
"SELECT overlay_generation, base_generation FROM overlay_generations "
"WHERE project = ?1 AND status = ?2 "
"ORDER BY overlay_generation ASC LIMIT 1;";
if (sqlite3_prepare_v2(s->db, select_sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
store_set_error_sqlite(s, "claim_ready_overlay_generation select prepare");
(void)cbm_store_rollback(s);
return CBM_STORE_ERR;
}
bind_text(stmt, ST_COL_1, project);
bind_text(stmt, ST_COL_2, CBM_STORE_OVERLAY_STATUS_READY);
rc = sqlite3_step(stmt);
if (rc == SQLITE_DONE) {
sqlite3_finalize(stmt);
(void)cbm_store_rollback(s);
return CBM_STORE_NOT_FOUND;
}
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
store_set_error_sqlite(s, "claim_ready_overlay_generation select");
(void)cbm_store_rollback(s);
return CBM_STORE_ERR;
}
int64_t overlay_generation = sqlite3_column_int64(stmt, 0);
int64_t base_generation = sqlite3_column_int64(stmt, ST_COL_1);
sqlite3_finalize(stmt);
stmt = NULL;
static const char update_sql[] =
"UPDATE overlay_generations SET status = ?3, updated_at = ?4 "
"WHERE project = ?1 AND overlay_generation = ?2 AND status = ?5;";
if (sqlite3_prepare_v2(s->db, update_sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
store_set_error_sqlite(s, "claim_ready_overlay_generation update 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);
sqlite3_bind_int64(stmt, ST_COL_2, overlay_generation);
bind_text(stmt, ST_COL_3, CBM_STORE_OVERLAY_STATUS_COMPACTING);
bind_text(stmt, ST_COL_4, ts);
bind_text(stmt, ST_COL_5, CBM_STORE_OVERLAY_STATUS_READY);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
store_set_error_sqlite(s, "claim_ready_overlay_generation update");
(void)cbm_store_rollback(s);
return CBM_STORE_ERR;
}
if (sqlite3_changes(s->db) != 1) {
store_set_error(s, "claim_ready_overlay_generation: ready generation not found");
(void)cbm_store_rollback(s);
return CBM_STORE_NOT_FOUND;
}
rc = cbm_store_commit(s);
if (rc != CBM_STORE_OK) {
(void)cbm_store_rollback(s);
return rc;
}
*out_overlay_generation = overlay_generation;
*out_base_generation = base_generation;
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};
+6
View File
@@ -688,6 +688,12 @@ int cbm_store_set_overlay_generation_status(cbm_store_t *s, const char *project,
const char *status);
int cbm_store_count_overlay_generations(cbm_store_t *s, const char *project,
const char *status, int *out_count);
/* Atomically claim the oldest ready overlay generation for compaction by
* moving it from overlay_ready to compacting. Returns CBM_STORE_NOT_FOUND when
* no ready overlay generation exists for the 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);
/* Publish one file's replacement facts into overlay storage. This does not
* mutate canonical nodes/edges; active read paths decide later how to combine
+94
View File
@@ -1288,6 +1288,91 @@ TEST(store_overlay_generation_rejects_invalid_inputs) {
PASS();
}
TEST(store_claim_ready_overlay_generation_claims_oldest_once) {
enum { BASE_GENERATION = 9 };
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 first = 0;
int64_t second = 0;
ASSERT_EQ(cbm_store_reserve_overlay_generation(s, "test", BASE_GENERATION, &first),
CBM_STORE_OK);
ASSERT_EQ(cbm_store_reserve_overlay_generation(s, "test", BASE_GENERATION + 1,
&second),
CBM_STORE_OK);
ASSERT_EQ(cbm_store_set_overlay_generation_status(s, "test", first,
CBM_STORE_OVERLAY_STATUS_READY),
CBM_STORE_OK);
ASSERT_EQ(cbm_store_set_overlay_generation_status(s, "test", second,
CBM_STORE_OVERLAY_STATUS_READY),
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, first);
ASSERT_EQ(base, BASE_GENERATION);
ASSERT_EQ(store_count_overlay_generation_row(s, "test", first, BASE_GENERATION,
CBM_STORE_OVERLAY_STATUS_COMPACTING),
1);
ASSERT_EQ(cbm_store_claim_ready_overlay_generation(s, "test", &claimed, &base),
CBM_STORE_OK);
ASSERT_EQ(claimed, second);
ASSERT_EQ(base, BASE_GENERATION + 1);
ASSERT_EQ(store_count_overlay_generation_row(s, "test", second, BASE_GENERATION + 1,
CBM_STORE_OVERLAY_STATUS_COMPACTING),
1);
claimed = -1;
base = -1;
ASSERT_EQ(cbm_store_claim_ready_overlay_generation(s, "test", &claimed, &base),
CBM_STORE_NOT_FOUND);
ASSERT_EQ(claimed, 0);
ASSERT_EQ(base, 0);
cbm_store_close(s);
PASS();
}
TEST(store_claim_ready_overlay_generation_ignores_nonready_and_validates_outputs) {
enum { BASE_GENERATION = 3, 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 reserved = 0;
int64_t failed = 0;
ASSERT_EQ(cbm_store_reserve_overlay_generation(s, "test", BASE_GENERATION,
&reserved),
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", failed,
CBM_STORE_OVERLAY_STATUS_FAILED),
CBM_STORE_OK);
int64_t claimed = SENTINEL;
int64_t base = SENTINEL;
ASSERT_EQ(cbm_store_claim_ready_overlay_generation(s, "test", &claimed, &base),
CBM_STORE_NOT_FOUND);
ASSERT_EQ(claimed, 0);
ASSERT_EQ(base, 0);
ASSERT_EQ(cbm_store_claim_ready_overlay_generation(s, "test", NULL, &base),
CBM_STORE_ERR);
ASSERT_EQ(base, 0);
claimed = SENTINEL;
ASSERT_EQ(cbm_store_claim_ready_overlay_generation(s, "test", &claimed, NULL),
CBM_STORE_ERR);
ASSERT_EQ(claimed, 0);
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();
@@ -1874,6 +1959,13 @@ TEST(store_compact_overlay_generation_promotes_metadata_and_cleans_overlay) {
ASSERT_EQ(cbm_store_reserve_index_generation(s, "test", NULL, NULL, &generation),
CBM_STORE_OK);
ASSERT_EQ(generation, COMPACT_GENERATION);
int64_t claimed_overlay = 0;
int64_t claimed_base = 0;
ASSERT_EQ(cbm_store_claim_ready_overlay_generation(s, "test", &claimed_overlay,
&claimed_base),
CBM_STORE_OK);
ASSERT_EQ(claimed_overlay, overlay_generation);
ASSERT_EQ(claimed_base, BASE_GENERATION);
ASSERT_EQ(cbm_store_compact_overlay_generation(s, "test", overlay_generation, generation),
CBM_STORE_OK);
@@ -5251,6 +5343,8 @@ SUITE(store_nodes) {
RUN_TEST(store_index_generation_finish_failed_and_invalid_status);
RUN_TEST(store_overlay_generation_reservation_status_and_counts);
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_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);