Adds restore_host_state(cluster_t*, const char*, uint32_t admin_status) to worker.c: OR's admin bits into an existing worker's status on hit; on miss, creates a minimal XCE_WORKER_UNAVAIL placeholder via makeWorker (mirroring the lost_and_found sentinel host precedent), with a valid non-NULL hostattrs_t so the host's later real join routes through rejoin_worker (not init_worker) and preserves the restored admin status. Idempotent under retry (find first, create only if missing). Standalone -- not yet wired into load_snapshot (task 4). Requirements: 1.10, 1.11, 5.8
361 lines
14 KiB
C
361 lines
14 KiB
C
/*
|
|
* test_upgrade_snapshot_restore.c — config-upgrade-snapshot task 3
|
|
*
|
|
* TDD tests for restore_host_state() (Requirements 1.10, 1.11, 5.8):
|
|
* restoring a host admin-state frame must work whether or not the target
|
|
* host has ever joined this member (create-if-missing, since hosts are
|
|
* join-provisioned, not config-provisioned -- Design Invariant 1), must be
|
|
* idempotent under a retried restore (Design Invariant 3), must never
|
|
* clobber a pre-existing worker's non-admin state on a hit, and the
|
|
* placeholder it creates on a miss must be safe for the host's later real
|
|
* join (routed through rejoin_worker by join.c's own existing
|
|
* key_find-based dispatch, unmodified) to adopt -- admin status set on the
|
|
* placeholder must still be set after that real join (Design Invariant 6).
|
|
*
|
|
* RED phase: restore_host_state does not exist yet, so this file fails to
|
|
* compile/link until Task 3 adds it to worker.c + worker.h.
|
|
*
|
|
* Fixture mirrors test_worker_join.c's bare in-memory cluster_t (workermgr +
|
|
* resmgr kvmgr instances, no disk/persist) -- restore_host_state only
|
|
* touches c->workermgr directly, and the placeholder-then-real-join test
|
|
* additionally needs the resmgr resource defs that apply_join_state's
|
|
* add_hostattrs_resvals()/set_host_slots() (invoked via rejoin_worker)
|
|
* require -- same resource-name list as test_worker_join.c.
|
|
*
|
|
* collect_confvars() (required for worker.c to link without pulling in the
|
|
* full dataex.c) is intentionally NOT redefined here: test_worker_join.c
|
|
* already provides it once for the whole controller_unit_test binary.
|
|
*/
|
|
|
|
#include <rtti.h>
|
|
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "cluster.h"
|
|
#include "kvmgr.h"
|
|
#include "resobj.h"
|
|
#include "wobj.h"
|
|
#include "worker.h"
|
|
#include "xce/cluster_def.h"
|
|
#include "xce/types.h"
|
|
#include "xstring.h"
|
|
#include "zarray.h"
|
|
|
|
/* ── Test fixture ────────────────────────────────────────────────────── */
|
|
|
|
/* Resource names/types matching add_hostattrs_resvals/set_host_slots in
|
|
* worker.c -- required so a real join (rejoin_worker) after a placeholder
|
|
* restore succeeds instead of failing resource lookups. */
|
|
#define N_RES 9
|
|
static const char* g_res_names[N_RES] = {
|
|
"type", "model", "cpuf", "maxcpus", "maxmem",
|
|
"maxswap", "maxtmp", "ndisks", "slots"
|
|
};
|
|
static const int g_res_valtypes[N_RES] = {
|
|
XCE_VAL_STRING, XCE_VAL_STRING,
|
|
XCE_VAL_NUMERIC, XCE_VAL_NUMERIC, XCE_VAL_NUMERIC,
|
|
XCE_VAL_NUMERIC, XCE_VAL_NUMERIC, XCE_VAL_NUMERIC,
|
|
XCE_VAL_NUMERIC
|
|
};
|
|
|
|
typedef struct {
|
|
cluster_t c;
|
|
var workermgr;
|
|
var resmgr;
|
|
} tur_cluster_t;
|
|
|
|
static tur_cluster_t g_tur;
|
|
|
|
static void tur_setup(void)
|
|
{
|
|
kvmgr_params_t str_params = {
|
|
.key_type = KVMGR_KEY_STRING,
|
|
.nslots = 64,
|
|
.delta = 16,
|
|
.datakiller = NULL,
|
|
};
|
|
|
|
/* Idempotent guard: the xstring intern table is process-global and
|
|
* shared across every test file in this binary (mirrors
|
|
* test_apply_seam.c / test_replay_apply_seam.c's guard, safer than
|
|
* test_worker_join.c's unconditional call since both files run in the
|
|
* same controller_unit_test process). */
|
|
if (makeString((char*)"tur-probe") == NULL)
|
|
xstring_table_init(139);
|
|
|
|
memset(&g_tur, 0, sizeof(g_tur));
|
|
|
|
g_tur.workermgr = makeInstance(&xkvmgr_type, &str_params);
|
|
TEST_ASSERT_NOT_NULL(g_tur.workermgr);
|
|
|
|
g_tur.resmgr = makeInstance(&xkvmgr_type, &str_params);
|
|
TEST_ASSERT_NOT_NULL(g_tur.resmgr);
|
|
|
|
for (int i = 0; i < N_RES; i++) {
|
|
xce_resource_t rdef;
|
|
memset(&rdef, 0, sizeof(rdef));
|
|
rdef.name = (char*)g_res_names[i];
|
|
rdef.valtype = g_res_valtypes[i];
|
|
rdef.flags = 0;
|
|
rdef.idx = -1;
|
|
|
|
xce_resource_t* r = makeResource(g_tur.resmgr, &rdef);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(r, g_res_names[i]);
|
|
}
|
|
|
|
g_tur.c.workermgr = g_tur.workermgr;
|
|
g_tur.c.resmgr = g_tur.resmgr;
|
|
zarray_init(&g_tur.c.sharedinst, 64, NULL);
|
|
}
|
|
|
|
static void tur_teardown(void)
|
|
{
|
|
zarray_fini(&g_tur.c.sharedinst);
|
|
if (g_tur.resmgr) dereference(g_tur.resmgr);
|
|
if (g_tur.workermgr) dereference(g_tur.workermgr);
|
|
memset(&g_tur, 0, sizeof(g_tur));
|
|
}
|
|
|
|
static worker_t* tur_find(const char* name)
|
|
{
|
|
worker_t* w = NULL;
|
|
lock(g_tur.c.workermgr, RTTI_LOCK_READ);
|
|
key_find(g_tur.c.workermgr, (void*)name, (void**)&w);
|
|
unlock(g_tur.c.workermgr);
|
|
return w;
|
|
}
|
|
|
|
/* ── Miss: create-if-missing placeholder (Req 1.10) ────────────────────── */
|
|
|
|
static void test_restore_host_state_creates_placeholder_for_unknown_host(void)
|
|
{
|
|
tur_setup();
|
|
|
|
int rc = restore_host_state(&g_tur.c, "never-joined-host",
|
|
XCE_WORKER_CLOSED);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
worker_t* w = tur_find("never-joined-host");
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(w,
|
|
"restore for a host with no local record must create exactly one placeholder");
|
|
TEST_ASSERT_TRUE_MESSAGE(w->host.status & XCE_WORKER_CLOSED,
|
|
"restored admin bit (CLOSED) must be set on the placeholder");
|
|
TEST_ASSERT_TRUE_MESSAGE(w->host.status & XCE_WORKER_UNAVAIL,
|
|
"placeholder must be unjoined/dead-until-connected");
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(w->host.attrs,
|
|
"placeholder hostattrs must be non-NULL (rejoin_worker "
|
|
"dereferences it directly on the later real join)");
|
|
|
|
tur_teardown();
|
|
printf("test_restore_host_state_creates_placeholder_for_unknown_host: PASS\n");
|
|
}
|
|
|
|
/* ── Idempotent retry after partial failure (Req 5.8) ──────────────────── */
|
|
|
|
static void test_restore_host_state_retry_is_idempotent(void)
|
|
{
|
|
tur_setup();
|
|
|
|
int rc1 = restore_host_state(&g_tur.c, "retry-host", XCE_WORKER_CLOSED);
|
|
TEST_ASSERT_EQUAL_INT(0, rc1);
|
|
|
|
worker_t* w1 = tur_find("retry-host");
|
|
TEST_ASSERT_NOT_NULL(w1);
|
|
int idx1 = w1->idx;
|
|
|
|
/* Simulate a retried restore (e.g. after a partial earlier failure) --
|
|
* same name/admin bits, must find-and-continue, never a duplicate
|
|
* create or an -EEXIST-style conflict. */
|
|
int rc2 = restore_host_state(&g_tur.c, "retry-host", XCE_WORKER_CLOSED);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc2,
|
|
"retried restore must not error");
|
|
|
|
worker_t* w2 = tur_find("retry-host");
|
|
TEST_ASSERT_NOT_NULL(w2);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(idx1, w2->idx,
|
|
"retry must reuse the same entry, never create a second one");
|
|
TEST_ASSERT_TRUE(w2->host.status & XCE_WORKER_CLOSED);
|
|
|
|
tur_teardown();
|
|
printf("test_restore_host_state_retry_is_idempotent: PASS\n");
|
|
}
|
|
|
|
/* ── Hit on a real, already-joined worker: OR bits only (Req 1.10) ──────── */
|
|
|
|
static void test_restore_host_state_on_real_worker_only_ors_admin_bits(void)
|
|
{
|
|
tur_setup();
|
|
|
|
hostattrs_t attrs;
|
|
memset(&attrs, 0, sizeof(attrs));
|
|
attrs.hosttype = makeString("LINUX");
|
|
attrs.hostmodel = makeString("x86");
|
|
attrs.cpufactor = 1.0f;
|
|
attrs.maxcpus = 4;
|
|
attrs.maxmem = 16;
|
|
attrs.maxswap = 32;
|
|
attrs.maxtmp = 64;
|
|
attrs.ndisks = 1;
|
|
|
|
xce_hostconfig_t hconfig;
|
|
memset(&hconfig, 0, sizeof(hconfig));
|
|
hconfig.hosttype = (char*)"!";
|
|
hconfig.hostmodel = (char*)"!";
|
|
hconfig.mxj = 4;
|
|
|
|
endpoint_t ep;
|
|
memset(&ep, 0, sizeof(ep));
|
|
|
|
int idx = init_worker(&g_tur.c, &ep, "real-host-1", &attrs, &hconfig);
|
|
TEST_ASSERT_GREATER_OR_EQUAL(0, idx);
|
|
|
|
worker_t* w = tur_find("real-host-1");
|
|
TEST_ASSERT_NOT_NULL(w);
|
|
TEST_ASSERT_FALSE(w->host.status & XCE_WORKER_UNAVAIL);
|
|
TEST_ASSERT_EQUAL_PTR(&ep, w->ep);
|
|
|
|
int rc = restore_host_state(&g_tur.c, "real-host-1", XCE_WORKER_CLOSED);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
TEST_ASSERT_TRUE_MESSAGE(w->host.status & XCE_WORKER_CLOSED,
|
|
"admin bit must be OR'd onto the real worker");
|
|
TEST_ASSERT_FALSE_MESSAGE(w->host.status & XCE_WORKER_UNAVAIL,
|
|
"restore on a real, already-joined worker must not mark it UNAVAIL");
|
|
TEST_ASSERT_EQUAL_STRING_MESSAGE("LINUX", w->host.attrs->hosttype,
|
|
"restore on a hit must not touch existing attrs");
|
|
TEST_ASSERT_EQUAL_PTR_MESSAGE(&ep, w->ep,
|
|
"restore on a hit must not touch ep");
|
|
|
|
tur_teardown();
|
|
printf("test_restore_host_state_on_real_worker_only_ors_admin_bits: PASS\n");
|
|
}
|
|
|
|
/* ── Observable (task 3's own line): placeholder -> later real join ────── */
|
|
|
|
static void
|
|
test_restore_host_state_placeholder_then_real_join_preserves_admin_status(void)
|
|
{
|
|
tur_setup();
|
|
|
|
/* Host admin-closed while down, then a configuration upgrade restores
|
|
* its admin-state frame on a member with no local record of the host's
|
|
* join -- must create a placeholder carrying the CLOSED bit. */
|
|
int rc = restore_host_state(&g_tur.c, "join-later-host",
|
|
XCE_WORKER_CLOSED);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
worker_t* w = tur_find("join-later-host");
|
|
TEST_ASSERT_NOT_NULL(w);
|
|
TEST_ASSERT_TRUE(w->host.status & XCE_WORKER_CLOSED);
|
|
TEST_ASSERT_TRUE(w->host.status & XCE_WORKER_UNAVAIL);
|
|
|
|
/* The host's real first join. join.c's own dispatch (join.c:90-103)
|
|
* does exactly: key_find() -> found w != NULL -> rejoin_worker() (never
|
|
* init_worker()). Reproduced directly here (rather than going through
|
|
* handle_worker_join) since that needs a live xce_request_t/endpoint_t/
|
|
* persist commit-barrier harness this unit test intentionally has none
|
|
* of -- the dispatch decision itself is not under test, join.c's is
|
|
* unmodified and already covered elsewhere; what's under test is that
|
|
* THIS placeholder is a valid, non-crashing input to rejoin_worker(). */
|
|
hostattrs_t new_attrs;
|
|
memset(&new_attrs, 0, sizeof(new_attrs));
|
|
new_attrs.hosttype = makeString("LINUX");
|
|
new_attrs.hostmodel = makeString("x86_64");
|
|
new_attrs.cpufactor = 1.0f;
|
|
new_attrs.maxcpus = 8;
|
|
new_attrs.maxmem = 32;
|
|
new_attrs.maxswap = 64;
|
|
new_attrs.maxtmp = 128;
|
|
new_attrs.ndisks = 2;
|
|
|
|
xce_hostconfig_t hconfig;
|
|
memset(&hconfig, 0, sizeof(hconfig));
|
|
hconfig.hosttype = (char*)"!";
|
|
hconfig.hostmodel = (char*)"!";
|
|
hconfig.mxj = 4;
|
|
|
|
endpoint_t ep;
|
|
memset(&ep, 0, sizeof(ep));
|
|
|
|
worker_t* found = NULL;
|
|
int findrc = 0;
|
|
lock(g_tur.c.workermgr, RTTI_LOCK_READ);
|
|
findrc = key_find(g_tur.c.workermgr, (void*)"join-later-host",
|
|
(void**)&found);
|
|
unlock(g_tur.c.workermgr);
|
|
TEST_ASSERT_GREATER_THAN_MESSAGE(-1, findrc,
|
|
"join.c's dispatch must find the placeholder (hit, not miss)");
|
|
TEST_ASSERT_EQUAL_PTR(w, found);
|
|
|
|
int idx = rejoin_worker(&g_tur.c, found, &ep, &new_attrs, &hconfig);
|
|
TEST_ASSERT_GREATER_OR_EQUAL_MESSAGE(0, idx,
|
|
"rejoin_worker must not fail on the placeholder");
|
|
|
|
TEST_ASSERT_TRUE_MESSAGE(w->host.status & XCE_WORKER_CLOSED,
|
|
"admin status set via restore must still be set after the real join");
|
|
TEST_ASSERT_FALSE_MESSAGE(w->host.status & XCE_WORKER_UNAVAIL,
|
|
"the real join must clear UNAVAIL");
|
|
TEST_ASSERT_EQUAL_STRING("LINUX", w->host.attrs->hosttype);
|
|
|
|
dereference(new_attrs.hosttype);
|
|
dereference(new_attrs.hostmodel);
|
|
|
|
tur_teardown();
|
|
printf("test_restore_host_state_placeholder_then_real_join_preserves_admin_status: PASS\n");
|
|
}
|
|
|
|
/* ── Defense-in-depth: non-admin bits are masked out ────────────────────── */
|
|
|
|
static void test_restore_host_state_masks_non_admin_bits(void)
|
|
{
|
|
tur_setup();
|
|
|
|
int rc = restore_host_state(&g_tur.c, "masked-host",
|
|
XCE_WORKER_CLOSED | XCE_WORKER_BUSY);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
worker_t* w = tur_find("masked-host");
|
|
TEST_ASSERT_NOT_NULL(w);
|
|
TEST_ASSERT_TRUE_MESSAGE(w->host.status & XCE_WORKER_CLOSED,
|
|
"admin bit must be applied");
|
|
TEST_ASSERT_FALSE_MESSAGE(w->host.status & XCE_WORKER_BUSY,
|
|
"non-admin bit must be masked out even if a caller passes it");
|
|
|
|
tur_teardown();
|
|
printf("test_restore_host_state_masks_non_admin_bits: PASS\n");
|
|
}
|
|
|
|
/* ── Input validation at the boundary ───────────────────────────────────── */
|
|
|
|
static void test_restore_host_state_rejects_empty_name(void)
|
|
{
|
|
tur_setup();
|
|
|
|
TEST_ASSERT_EQUAL_INT(-EINVAL,
|
|
restore_host_state(&g_tur.c, NULL, XCE_WORKER_CLOSED));
|
|
TEST_ASSERT_EQUAL_INT(-EINVAL,
|
|
restore_host_state(&g_tur.c, "", XCE_WORKER_CLOSED));
|
|
|
|
tur_teardown();
|
|
printf("test_restore_host_state_rejects_empty_name: PASS\n");
|
|
}
|
|
|
|
/* ── Dispatch ───────────────────────────────────────────────────────── */
|
|
|
|
void test_upgrade_snapshot_restore(void)
|
|
{
|
|
UnitySetTestFile(__FILE__);
|
|
printf("\n>> config-upgrade-snapshot host restore tests (task 3)\n\n");
|
|
|
|
RUN_TEST(test_restore_host_state_creates_placeholder_for_unknown_host);
|
|
RUN_TEST(test_restore_host_state_retry_is_idempotent);
|
|
RUN_TEST(test_restore_host_state_on_real_worker_only_ors_admin_bits);
|
|
RUN_TEST(test_restore_host_state_placeholder_then_real_join_preserves_admin_status);
|
|
RUN_TEST(test_restore_host_state_masks_non_admin_bits);
|
|
RUN_TEST(test_restore_host_state_rejects_empty_name);
|
|
}
|