feat(build-time-versioning): worker --version and startup log

Worker uses plain getopt_long(), not argp, so it has no free --version
handling like controller/scheduler. Adds a long-only OPT_VERSION=257
option and a handling case that computes xce_compose_version() into a
static buffer and returns before any startup work (check_user()/
xcecd_init/xcecd_start). Both startup INFO log branches (--peers and
no-peers) extended to log the identical buffer.

Live-verified: --version and both startup-log branches are
byte-identical (captured via a real daemon run under sudo).

Also updates bug-1519 (pre-existing, unrelated ASAN "Joining already
joined thread" abort during worker shutdown): independently reproduced
under real root during this task's review, refining its prior "never
reproduced under real root" note -- not a userns/unshare artifact, still
unfixed, does not affect this task's code path.

Task 3.3/13 of build-time-versioning. This closes out the "Core: C
daemon version display" phase (tasks 3.1-3.3).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-08-20 11:47:12 +00:00
parent 88649a0462
commit bc5c669b94
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
3 changed files with 41 additions and 15 deletions

View file

@ -40,10 +40,10 @@
- _Boundary: controller/main.c, scheduler/main.c, controller/testsuite/test.c_
- _Depends: 3.1_
- [ ] 3.3 (P) Worker: add --version and startup log line
- Add a long-only `--version` option to worker's `getopt_long()` option table and a handling case that prints the same macro output and exits before any other startup work
- Add the version string to worker's existing first startup INFO log line (its own `"HA cluster config..."` line)
- Observable: `xcecd --version` prints commit id + build time and exits without starting the daemon; the startup log's first lines include the identical two values
- [x] 3.3 (P) Worker: add --version and startup log line
- Added `OPT_VERSION=257` (long-only, beyond ASCII range, no collision with `"n:l:vh"`/`OPT_PEERS=256`); handling case computes `xce_compose_version()` into a `static char version_buf[128]` and returns before any startup work (`check_user()`/logdir/`xcecd_init`/`xcecd_start`)
- Both startup INFO log branches (`--peers` and no-`--peers`) extended to log the same buffer
- Observable confirmed live (independently re-verified by review): `xcecd --version` prints and exits; both startup log branches (captured via a real `sudo` daemon run) are byte-identical to `--version` output
- _Requirements: 1.1, 1.2, 2.1, 2.2, 3.1, 3.2, 4.3_
- _Boundary: worker/main.c_
- _Depends: 3.1_

View file

@ -15027,7 +15027,7 @@
"error_message": "==ERROR: AddressSanitizer: Joining already joined thread, aborting.",
"file": "src/worker/src/main.c (xcecd daemon startup)",
"root_cause": "UNRESOLVED / not root-caused. xcecd's root check (main.c:288 geteuid()!=0) was bypassed for sandbox testing via `unshare -r` (user-namespace fake root, euid=0 but not real root). Startup then immediately hit an ASAN double-pthread_join abort during/after the HA-cluster-config init log line (main.c:435), before any further logging. Never reproduced under real root -- unknown whether this is a genuine double-join bug in xcecd's daemonize/thread-spawn path (e.g. duplicate cleanup on an early-init error branch) or an artifact of unshare's user-namespace semantics (e.g. a syscall xcecd's threading depends on behaving differently under CLONE_NEWUSER, such as thread creation ordering or signal delivery). Blocked further component-failover-adoption Task 3.3 Item-3 E2E validation from getting a live worker registered, which in turn meant no real job ever produced a scheduler decision (picker/allocator never had a matching candidate to dispatch to or explicitly PEND against), so the scheduler's decision-publish/rejoin code path (task 3.2) could not be exercised via a fully live job in this sandbox.",
"fix": "Not fixed -- out of scope for Task 3.3 (which validates scheduler HA reconnect, not worker daemon startup). Worked around by validating scheduler join/failover/reconnect behavior via direct socket-state inspection (ss/proc fd) instead of a full live job pipeline; the decision-publish retry+rejoin logic itself already has white-box unit coverage from task 3.2 (test_scheduler_decision_publish_retry.c) and task 3.3 item 2 (test_scheduler_backpressure.c). If a future session needs a live worker in this sandbox, retry under real root (not unshare -r) first to isolate whether this is userns-specific.",
"fix": "Not fixed -- out of scope for Task 3.3 (which validates scheduler HA reconnect, not worker daemon startup). Worked around by validating scheduler join/failover/reconnect behavior via direct socket-state inspection (ss/proc fd) instead of a full live job pipeline; the decision-publish retry+rejoin logic itself already has white-box unit coverage from task 3.2 (test_scheduler_decision_publish_retry.c) and task 3.3 item 2 (test_scheduler_backpressure.c). If a future session needs a live worker in this sandbox, retry under real root (not unshare -r) first to isolate whether this is userns-specific. UPDATE (2026-08-20, build-time-versioning task 3.3 review): independently reproduced under REAL sudo root (not unshare -r) -- same signature, same timing (fires during later thread-join startup work, strictly AFTER the HA-cluster-config INFO log line, both with and without --peers). This refines/answers the prior 'never reproduced under real root' open question: it is NOT a userns/unshare-specific artifact -- the abort reproduces identically under genuine root. Still not root-caused/fixed; still does not block this session's version-display changes (main.c's new --version/log-line code runs entirely before the point where this abort fires).",
"tags": [
"asan",
"double-join",
@ -15039,8 +15039,8 @@
"component-failover-adoption"
],
"related_bugs": [],
"occurrences": 1,
"last_seen": "2026-08-11T12:49:17.815Z"
"occurrences": 2,
"last_seen": "2026-08-20T11:16:00Z"
},
{
"id": "bug-1520",

View file

@ -304,7 +304,8 @@ static int check_user(void)
}
/* Long-option identifiers (beyond ASCII range to avoid collisions) */
#define OPT_PEERS 256
#define OPT_PEERS 256
#define OPT_VERSION 257
int main(int argc, char* argv[])
{
@ -313,6 +314,15 @@ int main(int argc, char* argv[])
int verbose = 0;
__attribute__((unused)) char worker_log[1024] = { 0 };
const char* peers_str = NULL;
/* build-time-versioning task 3.3: worker uses plain getopt_long(), not
* argp, so there is no free argp_program_version auto-handling here
* (unlike controller/main.c, scheduler/src/main.c). version_buf is
* computed once, before argument parsing even starts, via the same
* xce_compose_version() runtime helper those two use -- both the
* --version case below and the startup INFO log lines further down
* read this SAME buffer, never recomputing it independently, so the
* two can never disagree (Requirement 2.2). */
static char version_buf[128];
xce_cluster_config_t cluster_cfg = {0};
int use_ha = 0; /* 1 when --peers given */
@ -356,11 +366,19 @@ int main(int argc, char* argv[])
char* logdir = NULL;
static struct option long_opts[] = {
{ "peers", required_argument, NULL, OPT_PEERS },
{ "help", no_argument, NULL, 'h' },
{ NULL, 0, NULL, 0 }
{ "peers", required_argument, NULL, OPT_PEERS },
{ "version", no_argument, NULL, OPT_VERSION },
{ "help", no_argument, NULL, 'h' },
{ NULL, 0, NULL, 0 }
};
/* Computed before argument parsing begins (mirrors controller/main.c's
* and scheduler/src/main.c's "compute before argp_parse()" ordering) so
* the OPT_VERSION case immediately below can print it, and so it is
* ready by the time the startup INFO log lines run later in main(). */
xce_compose_version(version_buf, sizeof(version_buf), "xce v0.0.1-",
STR(VERSION_COMMIT_ID), STR(VERSION_BUILD_TIME));
while ((opt = getopt_long(argc, argv, "n:l:vh", long_opts, NULL))
!= -1) {
switch (opt) {
@ -381,6 +399,12 @@ int main(int argc, char* argv[])
case OPT_PEERS:
peers_str = optarg;
break;
case OPT_VERSION:
/* Prints and exits before any other startup work (peer
* parsing, check_user(), daemon init, etc.) -- same contract
* as controller/scheduler's --version. */
printf("%s\n", version_buf);
return 0;
case 'h':
default:
printf("Usage: %s [-n name] [-l log_dir] [-v] "
@ -433,14 +457,16 @@ int main(int argc, char* argv[])
cluster_cfg.candidates_count, N_MAX_MASTERS);
}
XCECD_LOG(XCE_LOG_INFO,
"HA cluster config: %d candidate(s), identity=%s",
cluster_cfg.candidates_count, cluster_cfg.identity);
"HA cluster config: %d candidate(s), identity=%s, "
"version %s",
cluster_cfg.candidates_count, cluster_cfg.identity,
version_buf);
use_ha = 1;
} else {
XCECD_LOG(XCE_LOG_INFO,
"No --peers flag; using single-controller path "
"(host=%s port=%d)",
config.remote_host, config.remote_port);
"(host=%s port=%d), version %s",
config.remote_host, config.remote_port, version_buf);
}
/* P2.2 fix (component-failover-adoption, task 2.2, Req 1.4): assign
* cluster_cfg onto config BEFORE xcecd_init()/xcecd_start() run, so it