fix(worker-rejoin-resync): task 4.4 - gate periodic load-indices send on resync_pending

Extends task 4.3's post-join gating to resource_sync_thread()'s
independent periodic reporting pthread, closing the Requirement 3.8 gap
where load-indices sends could interleave with an in-flight job-state
reconciliation report. Also fixes two pre-existing latent bugs
(bug-1756, bug-1757) in resource_sync.c that blocked a truthful RED/GREEN
signal for the new tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ypUeKgbAqLWUkaSJ79poN
This commit is contained in:
Rongsong Shen 2026-09-02 03:01:56 +00:00
parent 4140ecdbae
commit b6a2f5ec14
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
4 changed files with 241 additions and 13 deletions

View file

@ -104,7 +104,7 @@
- _Boundary: xcecd_resync.c_
- _Depends: 4.2_
- [ ] 4.4 Gate the worker's other periodic background reporting on the same resync_pending flag
- [x] 4.4 Gate the worker's other periodic background reporting on the same resync_pending flag
- Close a Requirement 3.8 gap found by feature-level validation, missed by task 4.3's own boundary: `checking_connection()`'s `rejoin_call()` is not the worker daemon's only periodic post-join activity — `src/worker/src/resource_sync.c`'s `resource_sync_thread()` is a separate pthread, started once at daemon init (unrelated to any join event), that periodically (every `LOAD_INDICES_INTERVAL_DEFAULT`, 15s) sends live `xcecd_send_load_indices()`/`xcecli_update_resvals()` RPCs to the controller over the same connection used for the resync exchange, with zero reference to `resync_pending`/`xcecd_resync_is_pending()` anywhere in that file — confirmed by inspection
- Make the periodic load-indices send check `xcecd_resync_is_pending()` first, the same way `checking_connection()`'s `rejoin_call()` already does (task 4.3), and skip/defer that cycle's send while a reconciliation report is still in flight — the send is already periodic, so a skipped cycle is naturally retried on the next tick, no new retry/queueing mechanism needed
- Observable completion: a load-indices send that would otherwise fire while `resync_pending` is set is observed not to fire during that window, and resumes firing on its normal periodic cadence once the flag clears; the existing periodic cadence and behavior when no reconciliation is in flight are unchanged
@ -180,6 +180,7 @@
- Task 4.2: implemented `xcecd_resync_classify_job`/`xcecd_resync_build_report`/`xcecd_resync_answer_query` (classify RUNNING/FINISHED_OK/FINISHED_FAIL/UNRECOGNIZED, compute extras, paginate via task 1.4's `xce_job_state_report_req()`) as directly-callable, fully-tested functions (20/20 tests) — necessarily added one small, purely-additive companion function to task 4.1's API, `xcecd_finished_job_retention_snapshot()`, since 4.1 only exposed single-jobid lookup and 4.2 needs to enumerate ALL retained jobs for the extras set. Confirmed by inspection that nothing wires the worker's actual receipt of `OPCode_jobStateQuery` to call into this code yet (`controller_connector.c`'s `myreq_handler` has no case for it) — folded that wiring explicitly into task 4.3's scope (see its entry above), since 4.3 already needs the real receive path to implement its post-join gate correctly and is the same file/boundary.
- **Task 4.3 needed 2 review rounds — a genuine deadlock, not a style nitpick.** Round 1 placed the `resync_pending` gate check as `checking_connection()`'s entry guard (returning early before `xcecli_hello()`/the `needs_rejoin` check could run), so a worker whose connection dropped after a non-final page of a multi-page `OPCode_jobStateQuery` (no further page ever arriving to clear the gate) would have its ONLY connection-recovery mechanism permanently disabled — a direct Requirement 6.1 violation the existing suite didn't catch because no test drove the multi-page-then-disconnect sequence. Round 2 fixed it two ways: (a) narrowed the gate to guard only the `rejoin_call()` step inside the `needs_rejoin` branch, so `xcecli_hello()`/reconnect-detection always run every tick regardless of gate state; (b) added `xcecd_resync_reset()`, called from `xcecd_on_reconnect()` (the same signal that sets `needs_rejoin`), which clears the gate and discards any stalled partial multi-page accumulation the moment a real reconnect completes — bounding the deadlock window to at most one reconnect cycle instead of forever. Round 2's reviewer independently confirmed the fix by reintroducing the round-1 bug and rebuilding, and found the new *behavioral* regression test alone did NOT catch the reintroduced bug — only a new *structural* source-order test (asserting `xcecli_hello()` precedes the `needs_rejoin` check precedes the gate check, textually) actually distinguished correct from broken placement. **Lesson for future tasks in this codebase**: when a fix's correctness hinges on WHERE a check is placed relative to other statements (not just what value it reads), a pure behavioral/outcome test can pass even with the wrong placement if the test's specific stimulus happens not to exercise the broken ordering — add a structural/ordering assertion alongside the behavioral one for placement-sensitive gates.
- Task 4.3 (advisory, non-blocking, not fixed in this pass): `resync_pending`/`query_accum` in `xcecd_resync.c` are plain unsynchronized statics touched from two different OS threads — the worker's dedicated connector thread (`checking_connection()`/`xcecd_on_reconnect()`) and a `vtaskmgr` pool thread (`xcecd_resync_handle_query_page()`, reached via the request-dispatch path). This mirrors the pre-existing, design-directed unsynchronized `needs_rejoin` pattern (same codebase convention, deliberately reused per design.md), but is riskier since `xcecd_resync_reset()` calls `free()` on the accumulator rather than just staling a bool read — a narrow, non-deterministic window exists where an independent reconnect could free the buffer while a pool thread is mid-read of the same connection's in-flight query page. Does not reproduce task 4.3's own violated requirement (Req 6.1) and isn't required by any Requirement/AC as written; this project's ASAN/UBSAN test harness has no TSAN, so it also isn't mechanically testable the way the deadlock fix was. Worth a small follow-up (a mutex around the gate+accumulator, or moving accumulation to the connector thread only) if this ever needs hardening beyond what `needs_rejoin`'s own accepted risk profile already tolerates.
- **Task 4.4 found and fixed two pre-existing latent bugs** (bug-1756, bug-1757, both traced via `git log -S` to the original `cb0c7fe9` import, predating this feature) needed to get a truthful RED/GREEN signal for the new lifecycle-based tests: (1) `resource_sync_destroy()` unconditionally re-called `resource_sync_stop()`, double-`pthread_join`-ing the same thread handle (ASAN abort) — the exact sequence production `xcecd_cleanup()` already uses on shutdown; fixed by guarding on `atomic_load(&sync->running)`, mirroring `connection_handler.c`'s `network_handler_destroy()` idiom. (2) `init_builtin_load_indices()`'s own `static bool initialized` guard permanently blocked re-population after a `resource_sync_destroy()` cycle, contradicting its caller's own `g_load_indices == NULL` re-init check; removed as redundant/harmful. The gate itself deliberately does NOT reset `load_report_counter` on the resync_pending skip path (unlike the function's other skip branches) so a deferred send fires on the very next 1s tick once the flag clears rather than waiting a spurious extra `LOAD_INDICES_INTERVAL_DEFAULT` cycle — verified by hand-trace across 3 consecutive ticks by both the implementer and an independent reviewer (who also reproduced RED by reverting the gate and rebuilding, then restored GREEN), matching Requirement 3.8's "resumes ... once the flag clears" precisely.
- **Task 5 found and fixed bug-1751**: design.md's cited candidate-filter site (`scheduler.c:1211`'s `sched_reset_worker_()`, reached only via `sched_begin_session()`) is dead code on both live drivers — `driver.c`/`sim_loop.c` actually call `xce_core_begin_session()` (`core_session.c`), which runs an independently-maintained mirror, `core_reset_workers()`, not listed in design.md's File Structure Plan. Fixed by applying the identical `IS_WORKER_UNAVAIL || IS_WORKER_RECONCILING` disjunct to both sites (parity/dead-code-hygiene on the design-cited one, correctness on the actually-live one) rather than patching only the literally-cited site and leaving the task's own Observable Completion Criterion false in production — same category as bug-1746/bug-1747's design-vs-reality corrections. design.md's File Structure Plan corrected. Also confirmed design.md's Testing Strategy referenced an "existing IS_WORKER_UNAVAIL candidate-filter test" that does not actually exist; new coverage added to `test_core_session.c` instead (white-box `#include`s `core_session.c` already; `scheduler.c`'s own dependency closure is too large to white-box per established precedent, `test_snapshot_load_allocator_regression.c`) — `scheduler.c`'s copy of the fix is therefore verified only by compilation + the identical, tested logic pattern in its mirror, not by a direct test; judged acceptable since it's confirmed dead code today, not a live gap.
- **Task 6.1 found and fixed bug-1752**: the worker daemon's `myreq_handler` had NO dispatch case for `OPCode_push` at all before this task — every prior config push (including the pre-existing `clustername` field, in production since before this spec started) was silently discarded via `default: break;`. Confirmed genuine (diffed `myreq_handler` pre/post, ruled out an alternate consumer — `xcectx.c`'s `vtask_entry` only special-cases the unrelated generic `pushmsg` union arm, not `configpushmsg`) and confirmed `clustername` has never been read anywhere in `src/worker/`, so this was harmless dead infrastructure until `workerResyncWindowMs` became its first real consumer. Fixed narrowly — `handle_pushconfig()` consumes only `worker_resync_window_ms`, leaving `clustername`/`confvars` still unconsumed and explicitly out of this task's boundary — rather than expanding scope to wire up every existing-but-never-consumed push field. Ordering (join reply → config push → conditional arm+query) and the live-configured-value plumbing were both verified via genuine structural tests (real packets popped off the wire, two distinct configured values round-tripped), not weaker behavioral proxies.
- **Task 6.2's mandated standby-redirect investigation found bug-1753**, a DIFFERENT gap than the one it was looking for: the generic not-primary (`CTRL_STANDBY`) redirect path turned out to already work correctly with zero opcode-specific code (`ha_dispatch_gate` gates every opcode uniformly before the switch), but the investigation surfaced that `ha_dispatch_gate`'s SEPARATE `CTRL_PRIMARY`-without-write-quorum branch — a "fact-bearing ops admitted anyway" allowlist (`OPCode_notify`/`update`/`setpreason`/`clrpreason`, bug-396) — was missing `OPCode_jobStateReport`, meaning a report arriving during a transient quorum-loss window (e.g. mid-failover, this feature's own core motivating scenario) would be rejected outright with no retry, since the worker's send is fire-and-forget. Fixed by adding the one missing case, verified safe against `resync_round_is_current`'s staleness check and the WAL_MODE_RETAIN semantics every fact this feature produces already uses. Citation nit: this fix's true motivation sits closer to Requirements 1.3/4/6 than the task's own tagged 8.1/8.3 — recorded here for traceability, tasks.md's tag left as originally generated. **Lesson**: a task text that names one specific concern to investigate ("check the redirect path") can surface a DIFFERENT, more consequential gap in the same general area — don't stop the investigation at "the named concern turned out fine," keep reading the surrounding gating logic in full.

View file

@ -20610,6 +20610,58 @@
],
"occurrences": 1,
"last_seen": "2026-09-02T00:30:00Z"
},
{
"id": "bug-1756",
"timestamp": "2026-09-02T02:40:00Z",
"error_message": "worker-rejoin-resync task 4.4 (Requirement 3.8, resource_sync.c): AddressSanitizer abort \"Joining already joined thread, aborting.\" while building the task's own new resync_pending-gate tests for resource_sync_thread(). Discovered live: the new tests are the first callers ever to exercise resource_sync_create()/resource_sync_start()/resource_sync_stop()/resource_sync_destroy() as a full lifecycle from a test binary (the two pre-existing tests only ever called collect_load_indices() directly). Each test calls resource_sync_stop(sync) then resource_sync_destroy(sync) back-to-back -- the exact same sequence xcecd.c's own real shutdown path (xcecd_cleanup(), src/worker/src/xcecd.c:580-582) already uses in production.",
"file": "src/worker/src/resource_sync.c",
"root_cause": "resource_sync_stop() calls pthread_join(sync->thread, NULL) but never resets sync->thread to 0 (or any invalid sentinel) afterward. resource_sync_destroy() unconditionally calls resource_sync_stop() again as its first action, with no guard on whether the module was already stopped -- so a caller pattern of stop() then destroy() (used verbatim by both xcecd_cleanup() in production and the new task 4.4 tests) pthread_join()s the same already-joined thread handle a second time. This is undefined behavior; under ASAN it aborts hard instead of silently misbehaving. connection_handler.c's analogous network_handler_destroy() already guards its own equivalent second stop call with `if (handler->running)`, showing this exact hazard is a known pattern elsewhere in the same daemon that resource_sync.c's destroy simply never adopted.",
"fix": "FIXED (2026-09-02, worker-rejoin-resync task 4.4, in-boundary companion fix -- resource_sync.c was already in this task's file boundary and the double-join blocked the task's own new tests from ever completing). resource_sync_destroy() now guards its internal resource_sync_stop() call with `if (atomic_load(&sync->running))`, mirroring network_handler_destroy()'s established idiom exactly -- if the caller already stopped the module explicitly, destroy() no longer re-joins the same thread handle. Verified via 5 consecutive full runs of the rebuilt resource_sync_test binary under ASAN: 5 Tests, 0 Failures each run, no abort (previously deterministic abort on the very first run reaching a stop()+destroy() pair). Full worker suite (meson test --suite worker) also green: 7/7 OK, no regressions, including controller_connector_test and xcecd_resync_test (task 4.3's own suites).",
"tags": [
"worker-rejoin-resync",
"task-4.4",
"resource_sync-c",
"resource_sync_destroy",
"resource_sync_stop",
"asan",
"double-join",
"pthread",
"thread-lifecycle",
"companion-fix",
"requirement-3.8",
"fixed"
],
"related_bugs": [
"bug-1519",
"bug-1757"
],
"occurrences": 1,
"last_seen": "2026-09-02T02:40:00Z"
},
{
"id": "bug-1757",
"timestamp": "2026-09-02T02:45:00Z",
"error_message": "worker-rejoin-resync task 4.4 (resource_sync.c): after fixing bug-1756's double-join abort, the same new resync_pending-gate tests still failed deterministically -- \"No load indices to report\" logged, and both test_resource_sync_thread_resumes_load_indices_send_once_resync_clears and test_resource_sync_thread_sends_load_indices_normally_when_not_pending failed asserting the load-indices send call count stayed 0. Both tests run AFTER an earlier test in the same binary has already completed a full resource_sync_create()/start()/stop()/destroy() cycle.",
"file": "src/worker/src/resource_sync.c",
"root_cause": "init_builtin_load_indices() guarded its own body with a function-local `static bool initialized` that, once set true, permanently no-ops the function for the rest of the process -- but resource_sync_destroy() -> resource_sync_cleanup_load_indices() frees g_load_indices and resets it to NULL on every destroy. collect_load_indices() (the function's only caller) already re-checks `if (g_load_indices == NULL) { init_builtin_load_indices(); }` before every collection, correctly wanting to re-populate the registry after a destroy -- but the stale `initialized` static silently blocked that re-population forever after the first create/destroy cycle in a process, leaving g_load_indices permanently NULL/empty (a pure test-lifecycle gap: the real daemon calls resource_sync_create() exactly once per process lifetime, so it never previously exercised a second populate-after-teardown cycle).",
"fix": "FIXED (2026-09-02, worker-rejoin-resync task 4.4, in-boundary companion fix, same file as bug-1756 and blocking the same new tests). Removed the redundant function-local `static bool initialized` guard from init_builtin_load_indices() entirely -- its only caller, collect_load_indices(), already provides the exact same one-time-per-populated-window guarantee via its own `g_load_indices == NULL` check, and no other call site exists (grepped) and no concurrent caller (collect_load_indices() runs only on resource_sync_thread()'s single dedicated pthread in production, or single-threaded test code). Verified via 5 consecutive full runs of the rebuilt resource_sync_test binary: 5 Tests, 0 Failures each run (previously deterministic 1-2 failures on tests running after the first create/destroy cycle). Full worker suite green: 7/7 OK.",
"tags": [
"worker-rejoin-resync",
"task-4.4",
"resource_sync-c",
"init_builtin_load_indices",
"collect_load_indices",
"stale-guard",
"test-lifecycle",
"companion-fix",
"fixed"
],
"related_bugs": [
"bug-1756"
],
"occurrences": 1,
"last_seen": "2026-09-02T02:45:00Z"
}
]
}

View file

@ -2,6 +2,7 @@
#include "job_information.h"
#include "node_information.h"
#include "xcecd.h"
#include "xcecd_resync.h"
#include "xcecd_utils.h"
#include "xcelog.h"
#include <pthread.h>
@ -58,10 +59,23 @@ static void free_resvals(xce_resval_t* vals, int count)
static void init_builtin_load_indices(void)
{
static bool initialized = false;
if (initialized)
return;
/* bug-1756 (companion, worker-rejoin-resync task 4.4): this used to
* guard against re-running registration with its own function-local
* `static bool initialized`, but that guard is a duplicate of -- and
* conflicts with -- collect_load_indices()'s own `g_load_indices ==
* NULL` check, this function's only caller. Once g_load_indices is torn
* down by resource_sync_cleanup_load_indices() (resource_sync_destroy(),
* e.g. a create/start/stop/destroy cycle repeated within one process,
* as the task 4.4 tests below now do for the first time), the caller's
* check correctly wants to re-populate the registry, but the
* function-local `initialized` stayed stuck true from the first run and
* silently no-op'd forever after, leaving g_load_indices permanently
* NULL/empty for the rest of the process ("No load indices to report").
* Removed: the caller's own guard already ensures this body runs
* exactly once per populated-registry window, with no other call site
* (grepped) and no concurrent caller (collect_load_indices() only runs
* on resource_sync_thread()'s single dedicated pthread in production,
* or single-threaded test code). */
g_load_indices_capacity = MAX_LOAD_INDICES;
g_load_indices = (load_index_info_t*)calloc(g_load_indices_capacity,
sizeof(load_index_info_t));
@ -89,8 +103,6 @@ static void init_builtin_load_indices(void)
true);
resource_sync_register_load_index("io", Value_u_fvalue, node_info_get_io,
true);
initialized = true;
}
/**
@ -322,6 +334,30 @@ static void* resource_sync_thread(void* arg)
sync->load_report_counter++;
if (sync->load_report_counter >= LOAD_INDICES_INTERVAL_DEFAULT) {
/* worker-rejoin-resync task 4.4 (Requirement 3.8): this periodic
* load-indices send is worker post-join background activity,
* same as checking_connection()'s rejoin_call() (task 4.3,
* controller_connector.c) -- defer it while a job-state
* reconciliation report is still in flight rather than
* interleaving with it. Unlike the vals==NULL/valptrs==NULL
* skip branches below (genuine local failures, where resetting
* the counter avoids a tight failure-retry loop), this branch
* deliberately does NOT reset load_report_counter: the deferral
* is caused by an external condition outside this loop's
* control, of unknown duration, so the threshold stays crossed
* and the very next tick re-checks immediately once
* xcecd_resync_is_pending() clears -- resuming the send with no
* extra retry/backoff delay, rather than making a resync that
* just finished wait out a fresh full interval on top. */
if (xcecd_resync_is_pending()) {
XCECD_LOG(XCE_LOG_DEBUG,
"resource_sync_thread: deferring load-indices "
"send -- job-state resync report still being "
"sent");
sleep(SYNC_INTERVAL);
continue;
}
vals = collect_load_indices(&vals_count);
if (vals == NULL || vals_count <= 0) {
XCECD_LOG(XCE_LOG_ERROR, "No load indices to report");
@ -381,7 +417,17 @@ void resource_sync_destroy(resource_sync_t* sync)
XCECD_LOG(XCE_LOG_ERROR, "Invalid resource sync module");
return;
}
resource_sync_stop(sync);
/* bug-1756: resource_sync_stop() joins sync->thread but never resets the
* handle afterward, so an unconditional second call here double-joins
* the same pthread_t -- UB, caught by ASAN as "Joining already joined
* thread, aborting." xcecd.c's own shutdown sequence (xcecd_cleanup())
* calls resource_sync_stop() then resource_sync_destroy() back-to-back,
* hitting this exact hazard on every daemon shutdown. Guard the same
* way connection_handler.c's network_handler_destroy() already guards
* its own analogous stop-then-destroy call (`if (handler->running)`). */
if (atomic_load(&sync->running)) {
resource_sync_stop(sync);
}
resource_sync_cleanup_load_indices();
XCECD_LOG(XCE_LOG_DEBUG, "Resource sync module destroyed successfully");
FREEUP(sync);

View file

@ -28,11 +28,13 @@
*/
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <unity/unity.h>
#include "xcecd.h"
#include "xcecd_resync.h"
/* Real globals this white-box TU's resource_sync.c references
* (xcecd_ctx.virtual_name) -- not linking xcecd.c itself (its own
@ -62,20 +64,41 @@ char* node_info_get_mem(void) { return stub_getter(); }
char* node_info_get_tmp(void) { return stub_getter(); }
char* node_info_get_io(void) { return stub_getter(); }
/* Linker stub for controller_connector.c's real implementation -- only
* referenced by resource_sync_thread() (the background polling loop),
* never invoked by this test (which calls collect_load_indices()
* directly). */
/* Linker stub for controller_connector.c's real implementation, spied on by
* the worker-rejoin-resync task 4.4 tests below (Requirement 3.8) to observe
* whether resource_sync_thread()'s periodic loop actually attempted a send
* on a given tick. */
static int g_send_load_indices_call_count = 0;
bool xcecd_send_load_indices(xce_resval_t** vals, int count)
{
(void)vals;
(void)count;
g_send_load_indices_call_count++;
return true;
}
/* worker-rejoin-resync task 4.4 (Requirement 3.8): link-satisfying stub for
* xcecd_resync.c's public gate query -- this file's white-box target is
* resource_sync.c only, it does not #include xcecd_resync.c (that module's
* own resync_pending lifecycle is covered against the real production code
* by test_xcecd_resync.c). g_test_resync_pending lets the tests below
* control what resource_sync_thread()'s new gate check reads, same
* convention as test_controller_connector.c's own g_test_resync_pending
* double for the same real function. */
static bool g_test_resync_pending = false;
bool xcecd_resync_is_pending(void) { return g_test_resync_pending; }
#include "../src/resource_sync.c"
void setUp(void) { return; }
void setUp(void)
{
memset(&xcecd_ctx, 0, sizeof(xcecd_ctx));
xcecd_ctx.virtual_name = (char*)"pc13v";
g_test_resync_pending = false;
g_send_load_indices_call_count = 0;
}
void tearDown(void) { return; }
static void
@ -129,10 +152,116 @@ test_collect_load_indices_falls_back_to_gethostname_when_virtual_name_unset(
"virtual_name_unset: PASS\n");
}
/* ════════════════════════════════════════════════════════════════════════
* worker-rejoin-resync task 4.4 (Requirement 3.8): resource_sync_thread()'s
* periodic load-indices send is worker post-join background activity
* distinct from checking_connection()'s rejoin_call() (task 4.3, a separate
* pthread -- controller_connector.c's connector_to_controller() vs. this
* file's resource_sync_start()) -- it must also defer to an in-flight
* job-state reconciliation report rather than interleaving with it.
*
* resource_sync_thread() is a real pthread with its own SYNC_INTERVAL=1s
* sleep between checks; load_report_counter is seeded to
* LOAD_INDICES_INTERVAL_DEFAULT - 1 before starting the thread so the very
* first tick already crosses the reporting threshold, keeping these tests
* fast instead of waiting a full 15s interval.
* ════════════════════════════════════════════════════════════════════════ */
static void
test_resource_sync_thread_skips_load_indices_send_while_resync_pending(void)
{
resource_sync_t* sync = resource_sync_create();
TEST_ASSERT_NOT_NULL(sync);
g_test_resync_pending = true;
sync->load_report_counter = LOAD_INDICES_INTERVAL_DEFAULT - 1;
TEST_ASSERT_TRUE(resource_sync_start(sync));
/* Several ticks, resync_pending held true throughout -- proves the skip
* is not a one-shot fluke of the first check. */
sleep(2);
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, g_send_load_indices_call_count,
"a load-indices send must not fire while resync_pending is true "
"(Requirement 3.8)");
resource_sync_stop(sync);
resource_sync_destroy(sync);
printf("test_resource_sync_thread_skips_load_indices_send_while_resync_"
"pending: PASS\n");
}
static void
test_resource_sync_thread_resumes_load_indices_send_once_resync_clears(void)
{
resource_sync_t* sync = resource_sync_create();
TEST_ASSERT_NOT_NULL(sync);
g_test_resync_pending = true;
sync->load_report_counter = LOAD_INDICES_INTERVAL_DEFAULT - 1;
TEST_ASSERT_TRUE(resource_sync_start(sync));
sleep(2);
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, g_send_load_indices_call_count,
"sanity: no send should have fired yet while resync_pending was "
"true");
/* Simulates xcecd_resync_handle_query_page() finishing the report and
* clearing the gate (xcecd_resync.c, mirroring task 4.3's own
* resync_pending lifecycle). load_report_counter was never reset by
* the skip path above, so the very next tick's check should fire a
* send immediately -- no extra retry/backoff delay. */
g_test_resync_pending = false;
sleep(2);
TEST_ASSERT_TRUE_MESSAGE(
g_send_load_indices_call_count >= 1,
"the load-indices send must resume on the next periodic check once "
"resync_pending clears (Requirement 3.8)");
resource_sync_stop(sync);
resource_sync_destroy(sync);
printf("test_resource_sync_thread_resumes_load_indices_send_once_"
"resync_clears: PASS\n");
}
static void
test_resource_sync_thread_sends_load_indices_normally_when_not_pending(void)
{
resource_sync_t* sync = resource_sync_create();
TEST_ASSERT_NOT_NULL(sync);
g_test_resync_pending = false;
sync->load_report_counter = LOAD_INDICES_INTERVAL_DEFAULT - 1;
TEST_ASSERT_TRUE(resource_sync_start(sync));
sleep(2);
TEST_ASSERT_TRUE_MESSAGE(
g_send_load_indices_call_count >= 1,
"normal (non-reconciling) periodic load-indices reporting must be "
"unchanged by the new resync_pending gate");
resource_sync_stop(sync);
resource_sync_destroy(sync);
printf("test_resource_sync_thread_sends_load_indices_normally_when_not_"
"pending: PASS\n");
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_collect_load_indices_tags_host_with_virtual_name_not_gethostname);
RUN_TEST(test_collect_load_indices_falls_back_to_gethostname_when_virtual_name_unset);
RUN_TEST(test_resource_sync_thread_skips_load_indices_send_while_resync_pending);
RUN_TEST(test_resource_sync_thread_resumes_load_indices_send_once_resync_clears);
RUN_TEST(test_resource_sync_thread_sends_load_indices_normally_when_not_pending);
return UNITY_END();
}