myxce/src/lib/testsuite/test_kvmgr_reserve.c
Rongsong Shen dcf9f39c22 fix(job-array-single-wal-record): close bug-1443's bulk-insert partial-visibility gap
restore_job_array()'s bulk-insert pass could fail partway through
(key_add() on realloc growth exhaustion), leaving an already-inserted
prefix permanently visible in c->jobmgr but excluded from
out_new_jobs -- so those elements existed and were individually
addressable but never reached HOOK_DETAIL_JOB_NEW, never scheduled or
pushed, not on this attempt or any later successful retry. A literal
violation of Requirements 2.3 and 9.4, tracked as bug-1443.

Adds kvmgr_reserve()/xzarray_reserve(), new purely-additive primitives
in shared kvmgr.c/zarray.c (following the existing kvmgr_capture_keys()
template -- bare extern function, no RTTI vtable slot, caller-holds-
write-lock contract), that pre-grow c->jobmgr's backing store before
the bulk-insert pass begins, closing the dominant N-scaling failure
trigger structurally.

Found a genuine off-by-one in the existing, unmodified
xzarray_ensure_items(): its strict less-than sufficiency check means
reserving exactly array_size and then performing array_size real
stores still triggers one more growth cycle on the last store.
Reserving array_size + 1 is the exact minimal correct compensation,
independently re-derived and confirmed by review with concrete
arithmetic.

For the now-much-narrower residual case (kvtab_add()'s own small
per-entry allocation, or a content collision -- both non-N-scaling,
honestly documented as still theoretically possible): restore_job_array()
now populates out_new_jobs with whatever prefix was genuinely inserted
before a failure, and apply_committed_entry() fires HOOK_DETAIL_JOB_NEW
for that non-empty partial prefix even when the overall return is
negative, while still propagating the negative rc so the LSN correctly
halts and the Live Apply Resource Fence still engages for the
remainder. A genuinely-inserted element is never left un-pushed again.

Task 7.1 of job-array-single-wal-record (Requirements 2.3, 9.4, 9.6).
2026-07-16 15:08:02 +00:00

136 lines
4.9 KiB
C

/* test_kvmgr_reserve.c — job-array-single-wal-record task 7.1 (bug-1443)
*
* Unit tests for kvmgr_reserve() (kvmgr.c/kvmgr.h) -- the thin, kvmgr-level
* forwarding wrapper around xzarray_reserve() (zarray.c, see
* test_zarray_reserve.c for the rigorous capacity-growth proof at that
* lower level). This file proves kvmgr_reserve() itself:
* - correctly unwraps an xkvmgr_type instance and forwards to its
* internal value store, allowing a subsequent bulk key_add() sequence
* up to the reserved count to succeed;
* - propagates a simulated allocation failure (via the SAME
* xzarray_reserve_test_set_force_fail() seam kvmgr_reserve() forwards
* into -- there is no separate kvmgr-level seam, deliberately, since
* kvmgr_reserve() has no logic of its own beyond the forward);
* - rejects a NULL kvmgr / negative n defensively.
*/
#include <errno.h>
#include <rtti.h>
#include <stdio.h>
#include <stdlib.h>
#include <unity/unity.h>
#include "kv.h"
#include "kvmgr.h"
#include "zarray.h"
static var make_test_kvmgr(uint32_t nslots, uint32_t delta)
{
kvmgr_params_t params = { .key_type = KVMGR_KEY_INT,
.nslots = nslots,
.delta = delta,
.datakiller = NULL };
return makeInstance(&xkvmgr_type, &params);
}
/* ── kvmgr_reserve() allows a subsequent bulk key_add() sequence up to the
* reserved count to succeed (functional forwarding proof) ──────────────── */
static void test_kvmgr_reserve_allows_subsequent_bulk_key_add(void)
{
const int n = 30;
/* Tiny initial capacity/delta -- without reservation this would force
* many incremental growth cycles across the loop below. */
var kvmgr = make_test_kvmgr(2, 1);
TEST_ASSERT_NOT_NULL(kvmgr);
lock(kvmgr, RTTI_LOCK_WRITE);
int reserve_rc = kvmgr_reserve(kvmgr, n + 1);
TEST_ASSERT_TRUE_MESSAGE(reserve_rc >= 0,
"kvmgr_reserve must succeed for a valid kvmgr and count");
for (int i = 0; i < n; i++) {
int idx = key_add(kvmgr, (void*)(long)(1000 + i), (void*)(long)i);
TEST_ASSERT_TRUE_MESSAGE(idx >= 0,
"every key_add within the reserved count must succeed");
}
unlock(kvmgr);
/* Spot-check a few entries resolve correctly. */
void* found = NULL;
TEST_ASSERT_TRUE(key_find(kvmgr, (void*)(long)1000, &found) >= 0);
TEST_ASSERT_EQUAL_INT(0, (int)(long)found);
TEST_ASSERT_TRUE(key_find(kvmgr, (void*)(long)1029, &found) >= 0);
TEST_ASSERT_EQUAL_INT(29, (int)(long)found);
destroy(kvmgr);
printf("test_kvmgr_reserve_allows_subsequent_bulk_key_add: PASS\n");
}
/* ── Simulated allocation failure propagates through kvmgr_reserve() too --
* reusing the SAME xzarray_reserve()-level seam, since kvmgr_reserve() is a
* pure forward with no logic of its own. ───────────────────────────────── */
static void test_kvmgr_reserve_propagates_simulated_allocation_failure(void)
{
var kvmgr = make_test_kvmgr(4, 4);
TEST_ASSERT_NOT_NULL(kvmgr);
xzarray_reserve_test_set_force_fail();
lock(kvmgr, RTTI_LOCK_WRITE);
int rc = kvmgr_reserve(kvmgr, 10);
unlock(kvmgr);
TEST_ASSERT_EQUAL_INT_MESSAGE(-ENOMEM, rc,
"a forced allocation failure must propagate as -ENOMEM through"
" kvmgr_reserve()");
xzarray_reserve_test_clear_force_fail();
lock(kvmgr, RTTI_LOCK_WRITE);
int rc2 = kvmgr_reserve(kvmgr, 10);
unlock(kvmgr);
TEST_ASSERT_TRUE_MESSAGE(rc2 >= 0,
"clearing the force-fail seam must restore normal success");
destroy(kvmgr);
printf("test_kvmgr_reserve_propagates_simulated_allocation_failure: "
"PASS\n");
}
/* ── Defensive / boundary ─────────────────────────────────────────────── */
static void test_kvmgr_reserve_rejects_null_kvmgr(void)
{
int rc = kvmgr_reserve(NULL, 5);
TEST_ASSERT_TRUE(rc < 0);
printf("test_kvmgr_reserve_rejects_null_kvmgr: PASS\n");
}
static void test_kvmgr_reserve_rejects_negative_n(void)
{
var kvmgr = make_test_kvmgr(4, 4);
TEST_ASSERT_NOT_NULL(kvmgr);
lock(kvmgr, RTTI_LOCK_WRITE);
int rc = kvmgr_reserve(kvmgr, -1);
unlock(kvmgr);
TEST_ASSERT_TRUE(rc < 0);
destroy(kvmgr);
printf("test_kvmgr_reserve_rejects_negative_n: PASS\n");
}
/* ── Test group entry point ───────────────────────────────────────────── */
void test_kvmgr_reserve(void)
{
printf("\n>> kvmgr_reserve test\n");
RUN_TEST(test_kvmgr_reserve_allows_subsequent_bulk_key_add);
RUN_TEST(test_kvmgr_reserve_propagates_simulated_allocation_failure);
RUN_TEST(test_kvmgr_reserve_rejects_null_kvmgr);
RUN_TEST(test_kvmgr_reserve_rejects_negative_n);
}