feat(config-upgrade-snapshot): restore host admin-state frame safely (create-if-missing)

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
This commit is contained in:
Rongsong Shen 2026-07-24 12:48:28 +00:00
parent 387ea35c9f
commit 5b1600a8b2
6 changed files with 467 additions and 1 deletions

View file

@ -436,6 +436,105 @@ int restore_worker(void* cls, xce_worker_t host)
return idx;
}
/*
* restore_host_state — restore a host admin-state frame (config-upgrade-
* snapshot Requirements 1.10, 1.11, 5.8).
*
* Unlike restore_queue_state() (queues are config-provisioned, so an
* update-only lookup is always safe), a worker_t is join-provisioned: it
* may not exist yet on a member that hasn't locally replayed this host's
* original join record (e.g. one that received the upgrade snapshot via
* the out-of-band install path). So this restore path is create-if-missing:
*
* - Hit (host already registered -- a real joined worker, or an earlier
* placeholder left by a prior restore attempt): OR the restored admin
* bits into the existing status only. Nothing else is touched, so a
* real worker's live attrs/ep/resource values survive untouched.
* - Miss: create a minimal placeholder via makeWorker(), mirroring the
* synthetic lost_and_found sentinel host precedent exactly
* (config_repl.c:1114-1130) -- the same "makeWorker() without a join"
* mechanism, not a new one. The placeholder is marked
* XCE_WORKER_UNAVAIL (unjoined, dead-until-connected) and carries the
* restored admin bits. Its hostattrs_t is non-NULL with valid sentinel
* strings (copy_worker, wobj.c:53, requires non-NULL attrs; downstream
* apply_join_state()/add_hostattrs_resvals() treat hosttype/hostmodel
* as always-valid strings) so the host's later real join -- which
* join.c routes to rejoin_worker(), unmodified, because key_find() now
* finds this placeholder -- doesn't crash on rejoin_worker's direct
* attrs->hosttype dereference (worker.c:376-379).
*
* Idempotent under retry (5.8): find first via our own key_find, create
* only if truly missing. A second call for the same host (e.g. a retried
* restore after an earlier attempt failed partway through the snapshot)
* finds either the real worker or the already-created placeholder and just
* OR's the bits in again -- never a duplicate-create conflict.
*
* admin_status is masked to XCE_WORKER_ADMIN_MASK defensively (mirrors the
* collect side's own discipline, collect_host_frames only ever encoding
* admin-masked bits) so a caller passing an unmasked status word can never
* leak a non-admin bit (e.g. XCE_WORKER_BUSY) onto the restored host.
*/
int restore_host_state(cluster_t* c, const char* name, uint32_t admin_status)
{
worker_t* w = NULL;
int rc = 0;
if (name == NULL || name[0] == '\0')
return -EINVAL;
if (c == NULL || c->workermgr == NULL)
return -EINVAL;
admin_status &= XCE_WORKER_ADMIN_MASK;
/* No reference() pin across the workermgr-unlock/w-lock gap (unlike
* join.c's live-join path): this is called from checkpoint restore
* (ha_handle_snapshot_chunk -> persist_load_snapshot), which DOES run
* on the live vtask pool, not a quiesced phase, so a real first-time
* join for the same host CAN race this call. That race is still safe:
* a concurrent init_worker/makeWorker on a miss here would itself
* find-or-create via the same key_find/makeWorker machinery under
* workermgr's own lock, and either ordering converges to a worker
* with the admin bits OR'd in and UNAVAIL cleared by the real join's
* own apply_join_state() call -- the same "outer key_find, then
* unconditionally call into makeWorker" shape already used elsewhere
* in this codebase (worker_from_capnp/restore_worker,
* create_lost_and_found). Mirrors restore_queue_state's (queue.c:208)
* lock idiom, not a claim that this path is concurrency-free. */
lock(c->workermgr, RTTI_LOCK_READ);
rc = key_find(c->workermgr, (void*)name, (void**)&w);
unlock(c->workermgr);
if (rc < 0 && rc != -ENOENT)
return rc;
if (w != NULL) {
/* Hit: real joined worker or an earlier placeholder -- only OR the
* admin bits in, never touch anything else. */
lock(w, RTTI_LOCK_WRITE);
w->host.status |= admin_status;
unlock(w);
return 0;
}
/* Miss: create a minimal, valid placeholder -- mirrors the
* lost_and_found sentinel host precedent (config_repl.c:1114-1130). */
{
hostattrs_t placeholder_attrs = { .hosttype = "unknown",
.hostmodel = "unknown" };
xce_worker_t placeholder_host = { .name = (char*)name,
.attrs = &placeholder_attrs,
.status = XCE_WORKER_UNAVAIL
| admin_status };
w = makeWorker(c->workermgr, &placeholder_host);
if (w == NULL)
return -errno;
}
return 0;
}
struct worker_config_publish_args {
cluster_t* c;
endpoint_t* ep;