merge: sync Rongsong's scheduler-framework into chatbot

Full merge of origin/scheduler-framework (scheduler-core restructure,
failover-adoption, WAL_JOB_DISPATCH, apiserver backend health/heartbeat,
build-time versioning, bug-1673/1676/1678/1679/1680).

Conflict resolution:
- scheduler: adopt Rongsong's scheduler-core architecture (driver loop,
  queue project/picker fields, resource bug-1569 fix)
- worker/controller_connector.c: keep Rongsong's reconnect/rejoin
  structure, PORT our bug-1509 resend_job_status onto the rejoin success
  path (his reconnect didn't re-report job state)
- apiserver backend: take Rongsong's health/heartbeat (supersedes our
  e505066 circuit breaker)
- group.c: keep our bug-1513 comment
- misc: take theirs for additive fixes (go_build versioning,
  test_group.c, persist.c RUSAGE_UPDATE, FilterRows bug-1680)

NOTE: our scheduler fixes (bug-1494 family dispatch-fail requeue,
PEND-reason classification) are NOT in Rongsong's branch — needs
verification/porting onto scheduler-core (separate follow-up).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
lei.chen 2026-08-25 12:33:16 +00:00
commit 4e894d2184
395 changed files with 88517 additions and 5742 deletions

View file

@ -5,15 +5,13 @@ C daemon running on each compute node. Manages job execution, hardware monitorin
## Build
```bash
# From workspace root
nix develop
meson build && ninja -C build
/xce-build worker # lib + worker daemon (see build-deployment.md)
```
## Architecture
- `src/xcecd.c` — Main daemon entry point
- `src/hardware_monitor.c` — CPU/memory/GPU resource monitoring
- `src/process_manager.c` — Job process lifecycle (spawn, track, reap)
- `src/main.c` — Main daemon entry point (`int main`); links against `xcecd_srcs` from `src/xcecd.c` and friends
- `src/hardware_monitor.c` — CPU/memory resource monitoring (no GPU)
- `src/process_worker_manager.c` + `src/uv_proc.c` — Job process lifecycle (fork `xcecd_worker`, spawn/exec the job command); `src/process_manager.c` is unused dead code for job spawn (its `process_manager_start_process` is never called) but is still wired up for generic create/update/destroy/reap in `xcecd.c`
Links against `libxce` from `src/lib/`.

View file

@ -1,6 +0,0 @@
---
workerconf:
management_list: ["hostA", "hostB"]
controller_port: 9990
log_level: "info"
log_dir: "/tmp"

View file

@ -11,7 +11,6 @@
#include "system_monitor.h"
#include "thread_pool.h"
#include "uv_ipc.h"
#include "xcecd_conf.h"
#include "xcecd_db.h"
#include "xcecd_dbus_manager.h"
#include "xcecd_worker_utils.h"
@ -57,6 +56,13 @@ typedef struct {
int update_interval;
int domain_sock;
int debug_level;
/* HA candidate list parsed from --peers (component-failover-adoption,
* task 2.2, Req 1.4). Threaded through xcecd_init() -> xcecd_context_t
* -> controller_connector_init() so cc_ctx->cluster_cfg is populated
* before the first join, instead of main.c assigning it after
* xcecd_start() already ran. candidates_count == 0 means no --peers was
* given (single-host default). */
xce_cluster_config_t cluster_cfg;
} xcecd_config_t;
typedef struct {
@ -82,6 +88,11 @@ typedef struct {
char* log_dir;
char* db_path;
char* virtual_name;
/* Copied from xcecd_config_t by xcecd_init(); consumed by
* controller_connector_init() (xcecd_start()) to populate
* cc_ctx->cluster_cfg BEFORE controller_connector_start() fires the
* first join (component-failover-adoption, task 2.2, Req 1.4). */
xce_cluster_config_t cluster_cfg;
} xcecd_context_t;
extern xcecd_context_t xcecd_ctx;

View file

@ -1,47 +0,0 @@
#ifndef XCECD_CONF_H
#define XCECD_CONF_H
#include <cyaml/cyaml.h>
#include <stdbool.h>
#include <stdint.h>
#define DEFAULT_CONFIG_PATH "/opt/xce/conf/worker.yaml"
#define MAX_HOSTNAME_LEN 256
#define MAX_PATH_LEN 512
#define MAX_LOG_LEVEL_LEN 16
typedef enum {
CONFIG_SUCCESS = 0,
CONFIG_ERROR_FILE_NOT_FOUND,
CONFIG_ERROR_PARSE_FAILED,
CONFIG_ERROR_INVALID_DATA,
CONFIG_ERROR_MEMORY,
CONFIG_ERROR_UNKNOWN
} config_error_t;
typedef struct {
char* hostname;
} management_host_t;
typedef struct {
management_host_t* management_list;
uint32_t management_list_count;
uint16_t controller_port;
char* log_level;
char* log_dir;
} worker_config_t;
typedef struct {
config_error_t error_code;
char error_message[512];
worker_config_t* config;
} config_result_t;
config_result_t* config_load(const char* config_path);
config_result_t* config_load_default(void);
void config_free(config_result_t* result);
const char* config_error_string(config_error_t error);
void config_print(const worker_config_t* config);
int level_to_int(const char* level);
#endif // XCECD_CONF_H

View file

@ -67,7 +67,6 @@ xrun = executable('xrun',
xcecd_srcs = [
'src/static_array.c',
'src/xcecd_conf.c',
'src/inter_job_utils.c',
'src/xcecd_sig.c',
'src/uv_ipc.c',
@ -104,3 +103,5 @@ xcecd_virtual = executable('xcecd_virtual',
include_directories: [incdir],
dependencies:[xcecd_deps],
install:true)
subdir('testsuite')

View file

@ -8,7 +8,6 @@
#include "xcelog.h"
#include <errno.h>
#include <fcntl.h>
#include <pwd.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
@ -32,6 +31,23 @@ xcecli_ctx_t xcecli_ctx = { 0 };
xce_client_config_t xce_client_cfg = { 0 };
controller_connector_ctx_t* cc_ctx = NULL;
/* Persistent storage for the single candidate list the current connect/join
* attempt is driving (component-failover-adoption, task 2.1, Req 1 AC5).
* Must outlive the xcecli_ctx_t connection: xce_cluster_config_to_client_
* config() points xce_client_cfg.masters[] directly at this struct's
* hostname buffers (non-owning aliasing -- the library keeps a reference to
* xce_client_cfg for the life of the connection, so the source data behind
* it cannot be a stack-local temporary). Populated by build_cluster_config()
* below from either the operator-supplied --peers list or a synthesized
* single candidate -- never both, never a stack copy. */
static xce_cluster_config_t g_cluster_cfg = { 0 };
/* Set by the rejoin callback registered with the client library
* (xcecd_on_reconnect() below) when the library has transparently
* reconnected and this connector needs to re-issue xcecli_join() as a
* separate, later call. Consumed by checking_connection() (task 2.2). */
static bool needs_rejoin = false;
static int myreq_handler(xce_request_t*, void*);
/* make compiler happy */
@ -123,11 +139,17 @@ int start_controller_connector_thread(task_func_t function, void* arg)
}
/**
* @brief Initializes the client configuration for the controller connector.
* @brief Sets the caller-owned identity/auth fields (from, key) on the
* client config. Deliberately does NOT touch nmasters/masters[]/
* master_ports[] -- those are owned by the shared
* xce_cluster_config_to_client_config() helper (build_cluster_config()
* + the conversion call in xcecd_join_cluster() below), matching
* that helper's documented contract ("Does NOT set out->from /
* out->key -- caller sets those separately").
*
* @param config Pointer to the xce_client_config_t structure to be initialized.
* @param config Pointer to the xce_client_config_t structure to update.
*/
static void init_client_config(xce_client_config_t* config)
static void set_client_identity(xce_client_config_t* config)
{
int ret;
char local_host[MAXHOSTNAMELEN] = { 0 };
@ -142,9 +164,6 @@ static void init_client_config(xce_client_config_t* config)
strncpy(local_host, "localhost", sizeof(local_host) - 1);
}
config->nmasters = 1;
config->masters[0] = cc_ctx->remote_host;
config->master_ports[0] = cc_ctx->remote_port;
#ifdef VIRTUAL_CLUSTER
config->from = strdup(cc_ctx->virtual_name);
#else
@ -152,205 +171,335 @@ static void init_client_config(xce_client_config_t* config)
#endif
config->key = strdup("keepSecret");
XCECD_LOG(XCE_LOG_DEBUG, "Controller connector configuration initialized.");
XCECD_LOG(XCE_LOG_DEBUG,
"Join cluster configuration - Local host: %s, Remote host: %s, "
"Remote port: %d, Key: %s",
config->from, config->masters[0], config->master_ports[0],
config->key);
"Controller connector identity set: from=%s", config->from);
}
/**
* @brief Reconnect callback — invoked by libxce's transparent failover after
* a connection is re-established (and the ctx recovered), BEFORE the
* interrupted op resumes.
* @brief Build the single validated candidate list this connect/reconnect
* attempt will use (component-failover-adoption, task 2.1, Req 1
* AC5).
*
* This covers the case where the worker process stays alive and only the
* TCP link to the controller dropped (or the controller failed over): the
* failover retry loop in xcecli_hello() reconnects transparently and returns
* success, so checking_connection() sees ret==0 and never takes its
* xcecd_join_cluster()/resend_job_status() path. Without this hook, job
* state changes that occurred while disconnected are never re-reported and
* the controller is left with stale RUN/UNKWN jobs.
* A single controller is not a separately-maintained code path: when
* --peers configured a real candidate list (cc_ctx->cluster_cfg), it is
* used as-is; otherwise a one-candidate list is synthesized from the legacy
* remote_host/remote_port default. Either source is written into the same
* persistent g_cluster_cfg and then run through the identical
* xce_cluster_config_validate() call -- no branch downstream of this
* function distinguishes the two sources.
*
* @return 0 to let the interrupted op resume; <0 would be treated by the
* failover loop as "new primary rejected rejoin" and it would rotate.
* @return 0 on a validated config, the validator's negative error code
* (or -EINVAL if cc_ctx is unset) otherwise.
*/
static int xcecd_reconnect_cb(xcecli_ctx_t c, void* arg)
static int build_cluster_config(void)
{
(void)c;
(void)arg;
if (cc_ctx == NULL || cc_ctx->worker_list == NULL
|| *cc_ctx->worker_list == NULL) {
return 0;
if (!cc_ctx) {
return -EINVAL;
}
XCECD_LOG(XCE_LOG_INFO,
"Controller connection restored; resending job status");
if (cc_ctx->cluster_cfg.candidates_count > 0) {
g_cluster_cfg = cc_ctx->cluster_cfg;
} else {
memset(&g_cluster_cfg, 0, sizeof(g_cluster_cfg));
strncpy(g_cluster_cfg.candidates[0].hostname,
cc_ctx->remote_host ? cc_ctx->remote_host : "",
sizeof(g_cluster_cfg.candidates[0].hostname) - 1);
g_cluster_cfg.candidates[0].port = cc_ctx->remote_port;
g_cluster_cfg.candidates_count = 1;
strncpy(g_cluster_cfg.identity,
cc_ctx->virtual_name ? cc_ctx->virtual_name : "xcecd",
sizeof(g_cluster_cfg.identity) - 1);
}
/* Same semantics as checking_connection()'s rejoin resend (bug-1509
* B2/B3): report the ACTUAL job state so the controller reconciles its
* view (clears UNKWN on a live claim; converges to a terminal state for
* processes that are gone). */
(void)resend_job_status(*cc_ctx->worker_list);
return xce_cluster_config_validate(&g_cluster_cfg);
}
/* Sized for the worst case: MAX_HA_CANDIDATES entries, each up to
* (XCE_MAXHOSTNAME_LEN - 1) hostname chars + ':' + a 5-digit port + a 2-char
* ", " separator (263 bytes/candidate at the max) -- the extra +16 pads that
* comfortably rather than sizing exactly to the boundary. Named rather than
* inlined at each call site so the callers below and the tests that call
* format_candidate_list() directly can't drift out of sync with each other. */
#define CANDIDATE_LIST_BUF_SIZE ((XCE_MAXHOSTNAME_LEN + 16) * MAX_HA_CANDIDATES)
/**
* @brief Renders every candidate in g_cluster_cfg as "host:port, host:port,
* ..." into buf (component-failover-adoption, task 2.3, Req 1 AC6,
* Req 5 AC2).
*
* xcecd_join_cluster()'s all-candidates-exhausted failure paths only had a
* candidate COUNT to log ("all %d candidate(s) unreachable") -- neither
* xcecli_start() nor endpoint_connect() (src/lib/netio.c) log anything
* per-candidate themselves (confirmed by source read: no XCECD_LOG/INFO/WARN
* call anywhere in xcecli_start()'s per-master connect loop), so without
* this the worker's own failure message never actually named which
* candidates it tried, only how many -- a gap against Req 1 AC6 ("identifying
* which candidates were attempted") and Req 5 AC2 ("identifying every
* candidate attempted"). Found during task 2.3's all-candidates-unreachable
* coverage.
*
* Callers must size buf as CANDIDATE_LIST_BUF_SIZE (or larger) -- with an
* undersized buffer this truncates at the last candidate that fits rather
* than overflowing (bufsz - 1 bound below), so a too-small buf loses
* information, not memory safety.
*/
static void format_candidate_list(char* buf, size_t bufsz)
{
size_t used = 0;
buf[0] = '\0';
for (int i = 0; i < g_cluster_cfg.candidates_count && used < bufsz - 1;
i++) {
int written = snprintf(buf + used, bufsz - used, "%s%s:%d",
i > 0 ? ", " : "",
g_cluster_cfg.candidates[i].hostname,
g_cluster_cfg.candidates[i].port);
if (written < 0) {
break;
}
used += (size_t)written;
}
}
/**
* @brief Rejoin callback registered with the client library
* (component-failover-adoption, task 2.1, Req 2 AC1, Req 5 AC1-2).
*
* The library invokes this while still holding its own per-ctx lock
* (ac->failover.lock, a non-recursive mutex held for the whole
* XCECLI_FAILOVER_CALL retry loop) -- per design.md's "Rejoin callback
* constraint", this callback MUST NOT perform I/O or call any xcecli_*
* function: doing so would try to re-acquire the same already-held lock and
* self-deadlock the calling thread forever. It only sets a flag and returns
* 0 immediately so the interrupted operation resumes normally. Consuming
* this flag to issue the actual xcecli_join() as a separate, later call
* (once the triggering call has returned and the lock is free) is task
* 2.2's job.
*/
static int xcecd_on_reconnect(xcecli_ctx_t ctx, void* arg)
{
(void)ctx;
(void)arg;
needs_rejoin = true;
return 0;
}
/**
* @brief Handles I/O events for the endpoint, focusing on error conditions
* and connection liveness checks using EPOLLIN/EPOLLOUT.
* @brief Joins the cluster using the provided host attributes
* (component-failover-adoption, task 2.1, Req 1 AC1-3/AC5, Req 5
* AC1-2).
*
* Builds exactly one validated candidate list (build_cluster_config()),
* converts it via the shared libxce helper, and performs ONE
* init+start+join sequence. The client library's own failover mechanism
* (client_failover.c) rotates across candidates and follows standby
* redirects internally, within the single xcecli_join() call below --
* there is no per-candidate loop here anymore. A single configured
* candidate is simply the candidates_count=1 case of this exact sequence,
* not a separate fallback.
*
* @param attrs Pointer to the host attributes structure.
* @return int 0 on success, negative error code (or -1) on failure.
*/
int xcecd_join_cluster(hostattrs_t* attrs)
{
int rc;
if (!attrs) {
return -1;
}
rc = build_cluster_config();
if (rc < 0) {
XCECD_LOG(XCE_LOG_ERROR,
"xcecd_join_cluster: invalid cluster configuration (rc=%d)",
rc);
return rc;
}
XCECD_LOG(XCE_LOG_INFO,
"xcecd_join_cluster: attempting %d candidate(s), first=%s:%d",
g_cluster_cfg.candidates_count,
g_cluster_cfg.candidates[0].hostname,
g_cluster_cfg.candidates[0].port);
rc = xce_cluster_config_to_client_config(&g_cluster_cfg, &xce_client_cfg);
if (rc < 0) {
XCECD_LOG(XCE_LOG_ERROR,
"xcecd_join_cluster: candidate list conversion failed "
"(rc=%d)",
rc);
return rc;
}
set_client_identity(&xce_client_cfg);
rc = xcecli_ctx_init(&xcecli_ctx, &xce_client_cfg);
if (rc < 0) {
XCECD_LOG(XCE_LOG_ERROR,
"xcecd_join_cluster: xcecli_ctx_init failed (rc=%d)", rc);
return rc;
}
xcecli_set_handler(xcecli_ctx, &myreq_handler, NULL);
rc = xcecli_start(xcecli_ctx);
if (rc < 0) {
char attempted[CANDIDATE_LIST_BUF_SIZE] = { 0 };
format_candidate_list(attempted, sizeof(attempted));
XCECD_LOG(XCE_LOG_WARN,
"xcecd_join_cluster: all %d candidate(s) unreachable "
"(rc=%d): %s",
g_cluster_cfg.candidates_count, rc, attempted);
xcecli_ctx_fini(xcecli_ctx);
xcecli_ctx = NULL;
return rc;
}
/* Registered AFTER xcecli_start() succeeds, not before (task 2.3 fix --
* found during this task's E2E reconnect validation): xcecli_start()
* unconditionally resets ctx->failover.rejoin_cb/rejoin_arg to NULL as
* part of initializing per-context failover state (client.c, "no rejoin
* hook is installed for pure clients" -- it assumes callers register
* AFTER start, matching every xcecli_* client contract). Registering
* here BEFORE xcecli_start() (the code's shape prior to this fix) meant
* the registration was silently wiped out immediately afterward, so
* xcecd_on_reconnect() could never fire and needs_rejoin was never set
* by a real transparent reconnect -- task 2.1/2.2's whole rejoin-retry
* mechanism was dead in production despite passing unit tests (those
* tests set needs_rejoin directly or call xcecli_set_reconnect_handler()
* standalone, never through this exact xcecli_start()-then-register
* production sequence, so none of them could catch this ordering bug). */
xcecli_set_reconnect_handler(xcecli_ctx, xcecd_on_reconnect, NULL);
rc = xcecli_join(xcecli_ctx, HostRole_worker, attrs);
if (rc < 0) {
char attempted[CANDIDATE_LIST_BUF_SIZE] = { 0 };
format_candidate_list(attempted, sizeof(attempted));
errno = -rc;
XCECD_LOG(XCE_LOG_ERROR,
"xcecd_join_cluster: xcecli_join() failed across %d "
"candidate(s) (%s): %s",
g_cluster_cfg.candidates_count, attempted, strerror(errno));
xcecli_stop(xcecli_ctx);
xcecli_ctx_fini(xcecli_ctx);
xcecli_ctx = NULL;
return rc;
}
XCECD_LOG(XCE_LOG_INFO,
"xcecd_join_cluster: joined cluster (from=%s, %d "
"candidate(s) configured)",
xce_client_cfg.from, g_cluster_cfg.candidates_count);
return 0;
}
/**
* @brief Issues the actual rejoin call: the same join payload used at
* startup, as a separate call once the triggering call (xcecli_hello
* or whatever else invoked the reconnect callback) has returned and
* the client library's internal failover lock is free.
*
* Split out from checking_connection() behind the rejoin_call function
* pointer below purely for testability: xcecli_join() against a real
* candidate goes through XCECLI_FAILOVER_CALL's full retry/backoff budget
* (up to ~100s) on failure, and a genuine success requires a live
* capnp-speaking controller -- neither is practical to drive from a fast
* unit test. Production always runs through this exact function via the
* default rejoin_call pointer; only tests override the pointer to a
* controllable double.
*/
static int perform_pending_rejoin(void)
{
return xcecli_join(xcecli_ctx, HostRole_worker, &host_attrs);
}
/* Defaults to the real production call (see perform_pending_rejoin() above).
* Overridden only by tests. */
static int (*rejoin_call)(void) = perform_pending_rejoin;
/**
* @brief Periodic liveness probe on the connector thread
* (component-failover-adoption, task 2.1, Req 2 AC1-3, Req 3 AC1/AC3;
* task 2.2, Req 2 AC5, rejoin retry-on-failure).
*
* A single xcecli_hello() call. xcecli_hello() is XCECLI_FAILOVER_CALL-
* wrapped: on a transport failure it already retries indefinitely across
* the full configured candidate list (rotate + backoff) and invokes the
* registered rejoin-flag callback on its own -- the client library now
* owns connection recovery. This probe therefore no longer runs its own
* bounded retry-then-give-up loop: an outage that outlasts the old
* MAX_RETRY_COUNT*RETRY_INTERVAL bound no longer tears the worker's
* connection down for good (cc_ctx->running is never forced false here);
* the connector thread's normal periodic cadence simply gives the library
* another chance on the next call.
*
* Immediately after xcecli_hello() returns (its internal failover lock is
* now free), checks needs_rejoin -- set by xcecd_on_reconnect() when the
* library transparently reconnected during some earlier call. If set, issues
* xcecli_join() as a separate call (the same join payload used at startup).
* The flag clears ONLY on that join call's success (Req 2 AC5): if
* xcecli_join() itself returns -E_XCE_NOT_READY (its own retry budget
* exhausted) or any other failure, needs_rejoin stays set, so the worker is
* never left transport-connected but silently un-joined -- the next
* checking_connection() tick (this function's own periodic cadence, no new
* timer needed) retries the rejoin.
*
* Success is rc >= 0, NOT rc == 0 (task 2.3 fix -- found during this task's
* live E2E reconnect validation): xcecli_join()/xce_join() returns the
* controller's reply.status verbatim, and handle_worker_join()'s SUCCESS
* reply.status is the worker's assigned slot index (join.c, `reply.status =
* idx`), which is any non-negative integer, not always 0 -- only a genuine
* failure reply.status is negative. xcecd_join_cluster() above already
* treats its own xcecli_join() call this way (`if (rc < 0)` is its only
* failure test); this rejoin path used `rc == 0` instead, so a real,
* successful rejoin against a controller that happened to assign a non-zero
* worker index was misreported as a failure and retried forever (harmlessly
* idempotent, but never actually clearing needs_rejoin or logging success).
* Confirmed via a real 3-node cluster: after a primary kill and reconnect,
* xcecli_join() against the new primary returned rc=1 (a valid worker
* index) but the old `rc == 0` check logged "rejoin ... failed" on every
* subsequent liveness probe.
*
* @return int Always returns 0.
*/
int checking_connection()
{
const int RETRY_INTERVAL = 5; // seconds
int retry_count = 0;
int ret;
int ret = xcecli_hello(xcecli_ctx, "hello");
/* component-failover-adoption Req 2/Req 3: keep retrying indefinitely so
* the worker self-heals when its controller (or a failover primary)
* becomes reachable again — never give up after a bounded retry count and
* never call handle_disconnect() on a transient outage (that sets
* cc_ctx->running=false and exits the connector thread, requiring an
* operator restart). The connector thread is dedicated to this loop, so
* the unbounded retry/sleep cannot block the worker's other liveness
* duties (hardware/process monitoring run on separate threads). */
while (1) {
ret = xcecli_hello(xcecli_ctx, "hello");
if (ret == 0) {
// Connection successful
if (retry_count > 0) {
XCECD_LOG(XCE_LOG_DEBUG,
"Connection to controller restored after %d retries",
retry_count);
}
return 0;
}
retry_count++;
if (ret != 0) {
XCECD_LOG(XCE_LOG_ERROR,
"Connection to controller is broken. Retry %d failed",
retry_count);
"Connection to controller not currently healthy (rc=%d); "
"the client library will keep retrying/rotating "
"candidates in the background",
ret);
}
if (0 == xcecd_join_cluster(&host_attrs)) {
if (needs_rejoin) {
int rc = rejoin_call();
if (rc >= 0) {
needs_rejoin = false;
XCECD_LOG(XCE_LOG_INFO,
"Rejoined cluster successfully after retry %d",
retry_count);
"checking_connection: rejoin after transparent "
"reconnect succeeded");
/* bug-1509 Phase 3 (B2/B3): re-report running jobs on every
* rejoin, not just at startup, so the controller reconciles
* their state (clears UNKWN on a live claim; converges to a
* terminal state for processes that are gone). */
/* bug-1509 Phase 3 (B2/B3): re-report running/finished job state
* on every rejoin, not just at startup, so the controller
* reconciles its view after the disconnect window (clears UNKWN
* on a live claim; converges to a terminal state for processes
* that are gone). */
(void)resend_job_status(*cc_ctx->worker_list);
return 0;
} else {
XCECD_LOG(XCE_LOG_WARN,
"checking_connection: rejoin after transparent "
"reconnect failed (rc=%d); will retry on the next "
"liveness probe",
rc);
}
XCECD_LOG(XCE_LOG_INFO, "Retrying connection in %d seconds...",
RETRY_INTERVAL);
sleep(RETRY_INTERVAL);
}
}
/**
* @brief Joins the cluster using the provided host attributes.
*
* In HA mode (cc_ctx->cluster_cfg.candidates_count > 0), iterates through
* all candidates until one accepts the join as primary. In single-host mode
* the original path is taken unchanged.
*
* @param attrs Pointer to the host attributes structure.
* @return int 0 on success, -1 on failure.
*/
int xcecd_join_cluster(hostattrs_t* attrs)
{
if (!attrs) {
return -1;
}
int role = HostRole_worker;
/* HA mode: try each candidate until we find the primary */
if (cc_ctx != NULL && cc_ctx->cluster_cfg.candidates_count > 0) {
for (int i = 0; i < cc_ctx->cluster_cfg.candidates_count; i++) {
const char* host = cc_ctx->cluster_cfg.candidates[i].hostname;
int port = cc_ctx->cluster_cfg.candidates[i].port;
/* Point cc_ctx at this candidate so init_client_config() picks it up */
cc_ctx->remote_host = (char*)host;
cc_ctx->remote_port = port;
XCECD_LOG(XCE_LOG_INFO,
"xcecd_join_cluster: trying candidate %d: %s:%d",
i, host, port);
init_client_config(&xce_client_cfg);
xcecli_ctx_init(&xcecli_ctx, &xce_client_cfg);
xcecli_set_handler(xcecli_ctx, &myreq_handler, NULL);
int rc = xcecli_start(xcecli_ctx);
if (rc < 0) {
XCECD_LOG(XCE_LOG_WARN,
"xcecd_join_cluster: connect to %s:%d failed (rc=%d)",
host, port, rc);
xcecli_ctx_fini(xcecli_ctx); /* free context before next attempt */
continue;
}
rc = xcecli_join(xcecli_ctx, role, attrs);
if (rc == 0) {
XCECD_LOG(XCE_LOG_INFO,
"xcecd_join_cluster: joined primary at %s:%d",
host, port);
/* Re-report running job state when libxce transparently
* reconnects (failover) — checking_connection() only covers
* the explicit xcecd_join_cluster path, not the auto-reconnect
* path. Registered AFTER a successful join: xcecli_join also
* runs the failover loop, and a candidate-iteration rotate
* (standby redirect) must NOT fire the rejoin hook before the
* session exists. */
xcecli_set_reconnect_handler(xcecli_ctx, xcecd_reconnect_cb,
NULL);
return 0;
}
/* Standby replied — try next candidate */
XCECD_LOG(XCE_LOG_INFO,
"xcecd_join_cluster: candidate %s:%d returned %d "
"(likely standby redirect), trying next",
host, port, rc);
xcecli_stop(xcecli_ctx);
xcecli_ctx_fini(xcecli_ctx); /* free context before next attempt */
}
XCECD_LOG(XCE_LOG_ERROR,
"xcecd_join_cluster: all %d candidates failed",
cc_ctx->cluster_cfg.candidates_count);
return -1;
}
/* Single-host fallback (original path) */
init_client_config(&xce_client_cfg);
xcecli_ctx_init(&xcecli_ctx, &xce_client_cfg);
xcecli_set_handler(xcecli_ctx, &myreq_handler, NULL);
xcecli_start(xcecli_ctx);
int rc = xcecli_join(xcecli_ctx, role, attrs);
if (rc < 0) {
errno = -rc;
XCECD_LOG(XCE_LOG_ERROR, "Call xcecli_join() failure: %s",
strerror(errno));
return rc;
}
/* Same reconnect hook as the HA path above — after a successful join so a
* candidate-iteration rotate never fires it pre-session. */
xcecli_set_reconnect_handler(xcecli_ctx, xcecd_reconnect_cb, NULL);
return 0;
}
@ -358,12 +507,15 @@ int xcecd_join_cluster(hostattrs_t* attrs)
#ifndef XRUN
static void dump_startjob_request(xce_job_exec_t* d)
{
char jobidStr[64];
if (!d) {
return;
}
fmt_jobid(jobidStr, sizeof(jobidStr), d->jobid);
XCECD_LOG(XCE_LOG_DEBUG,
"Job ID: %lu, Command: %s, Exec User: %s, Exdata: %s", d->jobid,
"Job ID: %s, Command: %s, Exec User: %s, Exdata: %s", jobidStr,
d->start_info->cmdline ? d->start_info->cmdline : "NULL",
d->start_info->execuser ? d->start_info->execuser : "NULL",
d->start_info->exdata ? d->start_info->exdata : "NULL");
@ -538,11 +690,14 @@ static char* create_mpi_cmdline(char* cmd, char* type, xce_launch_t* launchinfo)
static int job_start_remote_task(uint64_t jobid, job_startinfo_t* startinfo,
xce_launch_t* launchinfo)
{
char jobidStr[64];
xce_taskhost_t hosts[launchinfo->nhosts - 1];
xce_taskrun_t taskrun;
int i;
int rc;
fmt_jobid(jobidStr, sizeof(jobidStr), jobid);
if (!startinfo || !launchinfo) {
XCECD_LOG(XCE_LOG_ERROR, "Invalid parameters for job_run_task_remote");
return -1;
@ -582,13 +737,13 @@ static int job_start_remote_task(uint64_t jobid, job_startinfo_t* startinfo,
taskrun.nhosts = launchinfo->nhosts - 1;
taskrun.jobid = jobid;
XCECD_LOG(XCE_LOG_DEBUG, "Launching %d remote tasks for job %lu",
taskrun.nhosts, jobid);
XCECD_LOG(XCE_LOG_DEBUG, "Launching %d remote tasks for job %s",
taskrun.nhosts, jobidStr);
xcecli_unblock_tasks(xcecli_ctx);
rc = xcecli_runtask(xcecli_ctx, &taskrun);
if (rc < 0) {
XCECD_LOG(XCE_LOG_ERROR, "Job %lu run remote task failed.", jobid);
XCECD_LOG(XCE_LOG_ERROR, "Job %s run remote task failed.", jobidStr);
}
} else {
rc = -ENOMEM;
@ -610,6 +765,7 @@ static int job_start_local_task(xce_request_t* request)
{
#ifndef XRUN
xce_job_exec_t* d = request->body.jobstartmsg;
char jobidStr[64];
dump_startjob_request(d);
@ -817,7 +973,8 @@ static int job_start_local_task(xce_request_t* request)
}
}
XCECD_LOG(XCE_LOG_DEBUG, "Job %lu ntasks is: %d", d->jobid,
fmt_jobid(jobidStr, sizeof(jobidStr), d->jobid);
XCECD_LOG(XCE_LOG_DEBUG, "Job %s ntasks is: %d", jobidStr,
job_ctx->ntasks);
/* Job limits */
@ -843,22 +1000,7 @@ static int job_start_local_task(xce_request_t* request)
}
if (job_ctx->startinfo->execuser != NULL) {
/* bug-1507: only use the exec user if it actually exists on this
* host. The API-layer submit user (e.g. "admin") is not
* necessarily a real /etc/passwd account, and systemd then fails
* the worker unit with EXIT_USER (217), killing every job. Fall
* back to nobody so the job still runs. */
struct passwd* pw = getpwnam(job_ctx->startinfo->execuser);
if (pw != NULL) {
job_ctx->limits->user_name
= strdup(job_ctx->startinfo->execuser);
} else {
job_ctx->limits->user_name = strdup("nobody");
XCECD_LOG(XCE_LOG_WARN,
"exec user '%s' does not exist on this host, "
"running job as nobody",
job_ctx->startinfo->execuser);
}
job_ctx->limits->user_name = strdup(job_ctx->startinfo->execuser);
} else {
job_ctx->limits->user_name = strdup("nobody");
XCECD_LOG(XCE_LOG_WARN,
@ -890,7 +1032,8 @@ static int job_start_local_task(xce_request_t* request)
static int handle_startjob(xce_request_t* request)
{
int rc = 0;
int rc = 0;
char jobidStr[64];
XCECD_LOG(XCE_LOG_DEBUG, "Received startjob request from controller");
@ -930,8 +1073,9 @@ static int handle_startjob(xce_request_t* request)
if (launchinfo->nhosts > 1) {
rc = job_start_remote_task(jobid, startinfo, launchinfo);
if (rc < 0) {
XCECD_LOG(XCE_LOG_ERROR, "Job %lu run remote task failed, rc %d.",
jobid, rc);
fmt_jobid(jobidStr, sizeof(jobidStr), jobid);
XCECD_LOG(XCE_LOG_ERROR, "Job %s run remote task failed, rc %d.",
jobidStr, rc);
xcecd_task_fail(jobid, request->body.runtaskmsg->clientidx, -1, rc);
return rc;
}
@ -955,6 +1099,7 @@ static int handle_runtasks(xce_request_t* request)
XCECD_LOG(XCE_LOG_DEBUG, "Received runtasks request from controller");
xce_taskrun_t* runmsg = request->body.runtaskmsg;
char jobidStr[64];
if (!runmsg) {
XCECD_LOG(XCE_LOG_ERROR,
@ -990,7 +1135,8 @@ static int handle_runtasks(xce_request_t* request)
return -1;
}
XCECD_LOG(XCE_LOG_DEBUG3, "run task: jobid = %lu", runmsg->jobid);
fmt_jobid(jobidStr, sizeof(jobidStr), runmsg->jobid);
XCECD_LOG(XCE_LOG_DEBUG3, "run task: jobid = %s", jobidStr);
XCECD_LOG(XCE_LOG_DEBUG3, "run task: user = %s",
runmsg->taskhosts[0]->execuser ? runmsg->taskhosts[0]->execuser
: "NULL");
@ -1230,10 +1376,12 @@ static int handle_fetchfile(xce_request_t* request)
int fd;
ssize_t nread;
uint32_t maxbytes;
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), msg->jobid);
XCECD_LOG(XCE_LOG_DEBUG,
"Fetchfile request: jobid=%lu path=%s offset=%lu max=%u",
msg->jobid, msg->filepath, msg->offset, msg->max_bytes);
"Fetchfile request: jobid=%s path=%s offset=%lu max=%u",
jobidStr, msg->filepath, msg->offset, msg->max_bytes);
memset(&freply, 0, sizeof(freply));
freply.jobid = msg->jobid;
@ -1307,6 +1455,7 @@ static int handle_jobcontrol(xce_request_t* request)
#ifndef XRUN
XCECD_LOG(XCE_LOG_DEBUG, "Received job control request from controller");
xce_jobctrl_t* d = request->body.jobctrlmsg;
char jobidStr[64];
if (d->jobid <= 0) {
XCECD_LOG(XCE_LOG_ERROR,
@ -1314,11 +1463,13 @@ static int handle_jobcontrol(xce_request_t* request)
return -1;
}
fmt_jobid(jobidStr, sizeof(jobidStr), d->jobid);
if (d->action == JobControl_u_siginfo) {
XCECD_LOG(
XCE_LOG_DEBUG,
"Received job control request to send SIGINFO for job ID: %lu",
d->jobid);
"Received job control request to send SIGINFO for job ID: %s",
jobidStr);
job_control_task_t* task_ctx = calloc(1, sizeof(job_control_task_t));
if (!task_ctx) {
@ -1339,8 +1490,8 @@ static int handle_jobcontrol(xce_request_t* request)
|| worker->job_status == JOB_STARTING) {
XCECD_LOG(XCE_LOG_ERROR,
"Job is pending or starting, controller should not "
"pass the signal to %lu.",
d->jobid);
"pass the signal to %s.",
jobidStr);
// really should not happen, but if it does, we handle it
// gracefully
@ -1349,8 +1500,8 @@ static int handle_jobcontrol(xce_request_t* request)
return 0;
}
} else {
XCECD_LOG(XCE_LOG_ERROR, "Failed to find worker for job ID: %lu",
d->jobid);
XCECD_LOG(XCE_LOG_ERROR, "Failed to find worker for job ID: %s",
jobidStr);
FREEUP(task_ctx);
return -1;
}
@ -1359,13 +1510,13 @@ static int handle_jobcontrol(xce_request_t* request)
if (!ipc_master_send_job_signal(task_ctx->jobid, task_ctx->signal,
task_ctx->force)) {
XCECD_LOG(XCE_LOG_ERROR,
"Failed to submit job control task for job ID: %lu",
d->jobid);
"Failed to submit job control task for job ID: %s",
jobidStr);
FREEUP(task_ctx);
return -1;
} else {
XCECD_LOG(XCE_LOG_DEBUG,
"Job control task submitted for job ID: %lu", d->jobid);
"Job control task submitted for job ID: %s", jobidStr);
}
FREEUP(task_ctx);
#else
@ -1374,8 +1525,8 @@ static int handle_jobcontrol(xce_request_t* request)
"Virtual cluster mode, handling job control task directly");
FREEUP(task_ctx);
XCECD_LOG(XCE_LOG_DEBUG,
"Job control task handled directly for job ID: %lu",
d->jobid);
"Job control task handled directly for job ID: %s",
jobidStr);
worker->killed_signal = d->u.siginfo->signo; // Mark job as killed
#endif
@ -1383,18 +1534,18 @@ static int handle_jobcontrol(xce_request_t* request)
#if !defined(VIRTUAL_CLUSTER)
XCECD_LOG(XCE_LOG_DEBUG,
"Received job control request to set checkpoint options for "
"job ID: %lu",
d->jobid);
"job ID: %s",
jobidStr);
if (!ipc_master_send_job_ckpt(d->jobid, d->u.ckptoption->kill,
d->u.ckptoption->method)) {
XCECD_LOG(XCE_LOG_ERROR,
"Failed to send checkpoint options for job ID: %lu",
d->jobid);
"Failed to send checkpoint options for job ID: %s",
jobidStr);
return -1;
} else {
XCECD_LOG(XCE_LOG_DEBUG, "Checkpoint options sent for job ID: %lu",
d->jobid);
XCECD_LOG(XCE_LOG_DEBUG, "Checkpoint options sent for job ID: %s",
jobidStr);
}
#else
XCECD_LOG(XCE_LOG_WARN, "Received job control request in virtual "
@ -1402,8 +1553,8 @@ static int handle_jobcontrol(xce_request_t* request)
#endif
} else {
XCECD_LOG(XCE_LOG_ERROR,
"Received unknown job control action: %d for job ID: %lu",
d->action, d->jobid);
"Received unknown job control action: %d for job ID: %s",
d->action, jobidStr);
return -1;
}
#endif // XRUN
@ -1485,8 +1636,10 @@ bool controller_connector_start(void)
int retry_limit = 0;
static int error_count = 0;
init_client_config(&xce_client_cfg);
/* No standalone client-config construction here anymore -- the single
* synthesize-validate-convert-connect pipeline inside
* xcecd_join_cluster() below builds and sets xce_client_cfg itself on
* every attempt (component-failover-adoption, task 2.1, Req 1 AC5). */
do {
rc = xcecd_join_cluster(&host_attrs);
if (rc < 0) {
@ -1630,7 +1783,10 @@ int xcecd_job_remote_start(uint64_t jobid,
const char* hostname,
const char* command)
{
int rc = 0;
int rc = 0;
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), jobid);
if (!cc_ctx || !cc_ctx->worker_list) {
XCECD_LOG(XCE_LOG_ERROR,
@ -1654,7 +1810,7 @@ int xcecd_job_remote_start(uint64_t jobid,
jobid);
if (!worker) {
XCECD_LOG(XCE_LOG_ERROR,
"Worker not found for job ID %lu", jobid);
"Worker not found for job ID %s", jobidStr);
return -1;
}
@ -1682,15 +1838,15 @@ int xcecd_job_remote_start(uint64_t jobid,
rc = xcecli_runtask(xcecli_ctx, &taskrun);
if (rc < 0) {
XCECD_LOG(XCE_LOG_ERROR,
"Failed to start job %lu on remote host %s, rc: %d",
jobid, hostname, rc);
"Failed to start job %s on remote host %s, rc: %d",
jobidStr, hostname, rc);
xcecd_task_fail(jobid, -1, -1, rc);
return rc;
}
XCECD_LOG(XCE_LOG_DEBUG,
"Remote task of job %lu will run on %s",
jobid, hostname);
"Remote task of job %s will run on %s",
jobidStr, hostname);
return 0;
}
@ -1718,14 +1874,17 @@ int xcecd_jobpost_done(uint64_t jobid)
int xcecd_jobrusage_update(xce_job_rusages_t* rusages)
{
char jobidStr[64];
if (!rusages) {
XCECD_LOG(XCE_LOG_ERROR,
"Invalid rusage data.");
return -1;
}
fmt_jobid(jobidStr, sizeof(jobidStr), rusages->jobid);
XCECD_LOG(XCE_LOG_DEBUG,
"Updating job rusage to controller for job %lu", rusages->jobid);
"Updating job rusage to controller for job %s", jobidStr);
return xcecli_update_jusage(xcecli_ctx,
rusages->jobid,

View file

@ -157,15 +157,20 @@ static void handle_message_from_queue(void)
static void master_disconnect_callback(uint64_t job_id)
{
XCECD_LOG(XCE_LOG_DEBUG, "Worker with client ID %lu disconnected", job_id);
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), job_id);
XCECD_LOG(XCE_LOG_DEBUG, "Worker with client ID %s disconnected", jobidStr);
}
static void master_job_status_callback(job_status_t* job_status)
{
int ret = XCECD_OK;
int ret = XCECD_OK;
char jobidStr[64];
XCECD_LOG(XCE_LOG_INFO, "Job %lu status: %d, exdata: %s, %s",
job_status->job_id, job_status->status_code,
fmt_jobid(jobidStr, sizeof(jobidStr), job_status->job_id);
XCECD_LOG(XCE_LOG_INFO, "Job %s status: %d, exdata: %s, %s",
jobidStr, job_status->status_code,
job_status->start_exdata[0] ? job_status->start_exdata
: "no exdata",
job_status->status_message);
@ -222,10 +227,13 @@ static void master_heartbeat_callback(uv_stream_t* client)
static void master_ckpt_result_callback(uint64_t job_id, int status,
const char* restore_cmd)
{
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), job_id);
XCECD_LOG(
XCE_LOG_DEBUG,
"Received checkpoint result for job %lu: status %d, restore_cmd: %s",
job_id, status, restore_cmd ? restore_cmd : "NULL");
"Received checkpoint result for job %s: status %d, restore_cmd: %s",
jobidStr, status, restore_cmd ? restore_cmd : "NULL");
if (job_id <= 0) {
XCECD_LOG(XCE_LOG_ERROR, "Job ID is not correct.");
@ -238,12 +246,15 @@ static void master_ckpt_result_callback(uint64_t job_id, int status,
static void master_rusages_callback(xce_job_rusages_t* rusages)
{
char jobidStr[64];
if (!rusages) {
return;
}
XCECD_LOG(XCE_LOG_DEBUG, "Received rusage for job %lu with %d metrics.",
rusages->jobid, rusages->count);
fmt_jobid(jobidStr, sizeof(jobidStr), rusages->jobid);
XCECD_LOG(XCE_LOG_DEBUG, "Received rusage for job %s with %d metrics.",
jobidStr, rusages->count);
xcecd_jobrusage_update(rusages);
@ -280,31 +291,62 @@ static int check_xce_server_dir(char* myfile)
static int check_user(void)
{
#ifndef VIRTUAL_CLUSTER
/* Root is required for the real production daemon (job execution as
* arbitrary users, cgroup management, etc.). This check is fully
* unconditional here -- zero runtime bypass surface -- for the real
* `xcecd` binary (worker/meson.build, no VIRTUAL_CLUSTER). */
if (geteuid() != 0) {
fprintf(stderr, "FATAL: xcecd should be run as root");
exit(-1);
}
#endif
/* VIRTUAL_CLUSTER (xcecd_virtual, worker/meson.build) already skips the
* sqlite/db-recovery block and other root-dependent subsystems (see
* xcecd_start()), so it does not need real root -- this lets it run as
* an ordinary user in a sandboxed dev/CI environment with no root/sudo
* and no permission to create a user namespace (unshare --map-root-user
* fails with EPERM there), which component-failover-adoption task 2.3
* relies on to drive a REAL worker process against a REAL local HA
* controller cluster for its reconnect E2E test. Decided at build time,
* not by an environment variable reachable in the production binary. */
return 1;
}
/* 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[])
{
int opt;
config_result_t* config_result = NULL;
int ret = XCECD_OK;
int config_flag = 0;
char* custom_config_path = NULL;
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];
/* dirty-content-fingerprint task 3.3: SEPARATE buffer/composition, used
* ONLY for the --version case below. version_buf above is left
* completely untouched -- its own xce_compose_version() call and the
* two mutually-exclusive startup INFO log lines reading it (further
* down) are unaffected by this feature, per Requirement 3.2. This
* divergence (the two call sites now showing different strings) is
* deliberate, not an oversight -- mirrors controller/main.c's and
* scheduler/src/main.c's identical version_display_buf pattern
* (dirty-content-fingerprint task 3.2). */
static char version_display_buf[128];
xce_cluster_config_t cluster_cfg = {0};
int use_ha = 0; /* 1 when --peers given */
check_user();
signal(SIGPIPE, SIG_IGN);
struct sigaction sa;
@ -335,7 +377,7 @@ int main(int argc, char* argv[])
.domain_sock_file = "/var/run/.xcecd.sock",
.virutal_name = "localhost01",
.remote_host = "localhost",
.remote_port = 6789,
.remote_port = 9990,
.join_interval = 5,
.update_interval = 5,
.resource_update_interval = 1,
@ -344,89 +386,36 @@ int main(int argc, char* argv[])
char logfile[1024] = { 0 };
char* logdir = NULL;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-c") == 0 && i + 1 < argc) {
custom_config_path = argv[i + 1];
config_flag = 1;
break;
}
}
if (config_flag == 0) {
custom_config_path = strdup(DEFAULT_CONFIG_PATH);
}
if (access(custom_config_path, F_OK) == 0) {
/* load config from custom file */
config_result = config_load(custom_config_path);
if (!config_result) {
fprintf(stderr, "Failed to load config from %s\n",
custom_config_path);
return -1;
}
if (config_result->error_code == CONFIG_SUCCESS) {
if (config_result->config->controller_port > 0) {
config.remote_port = config_result->config->controller_port;
}
if (config_result->config->log_dir) {
logdir = strdup(config_result->config->log_dir);
}
if (config_result->config->log_level) {
config.debug_level
= level_to_int(config_result->config->log_level);
}
if (config_result->config->management_list_count > 0) {
config.remote_host
= config_result->config->management_list[0].hostname;
}
}
} else {
fprintf(stderr, "Configuration file %s not found.\n",
custom_config_path);
return -1;
}
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 }
};
while ((opt = getopt_long(argc, argv, "c:p:m:n:l:h", long_opts, NULL))
/* 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));
/* dirty-content-fingerprint task 3.3: --version-only composition --
* appends the dirty_fingerprint suffix (if any) on top of the same
* commit/build_time inputs. Do NOT fold this into version_buf above; the
* startup INFO log lines must keep showing the original, fingerprint-
* less format (Req 3.2). */
xce_compose_version_ex(version_display_buf, sizeof(version_display_buf),
"xce v0.0.1-", STR(VERSION_COMMIT_ID),
STR(VERSION_BUILD_TIME),
STR(VERSION_DIRTY_FINGERPRINT));
while ((opt = getopt_long(argc, argv, "n:l:vh", long_opts, NULL))
!= -1) {
switch (opt) {
case 'c':
break; /* Custom config file already handled above */
case 'p':
if (config_flag) {
fprintf(stderr, "Custom config file already specified, "
"ignoring -p option.\n");
break;
}
config.remote_port = atoi(optarg);
break;
case 'm':
if (config_flag) {
fprintf(stderr, "Custom config file already specified, "
"ignoring -m option.\n");
break;
}
config.remote_host = optarg;
break;
case 'n':
config.virutal_name = optarg;
break;
case 'l':
if (config_flag) {
fprintf(stderr, "Custom config file already specified, "
"ignoring -l option.\n");
break;
}
if (optarg) {
if (logdir) {
FREEUP(logdir);
@ -434,26 +423,49 @@ int main(int argc, char* argv[])
logdir = strdup(optarg);
}
break;
case 'v':
verbose++;
break;
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. dirty-content-fingerprint
* task 3.3: prints version_display_buf (fingerprint-aware), not
* version_buf -- the startup INFO log lines further down keep
* reading the original version_buf, untouched. */
printf("%s\n", version_display_buf);
return 0;
case 'h':
default:
#ifdef VIRTUAL_CLUSTER
printf("Usage: %s -n virtual_name [-p remote_port] [-m "
"remote_host] [-l log_dir] [--peers list] "
"[-h help]\n",
argv[0]);
#else
printf("Usage: %s [-p remote_port] [-m remote_host] [-l log_dir] "
printf("Usage: %s [-n name] [-l log_dir] [-v] "
"[--peers ctrl-1=host:port,...] "
"[-h help]\n",
argv[0]);
#endif
return -1;
return 0;
}
}
/* Map -v count to log level (same pattern as controller/scheduler).
* 0=INFO (default), 1=DEBUG, 2=DEBUG2, 3+=DEBUG3. */
if (verbose >= 3) {
config.debug_level = XCE_LOG_DEBUG3;
} else if (verbose == 2) {
config.debug_level = XCE_LOG_DEBUG2;
} else if (verbose == 1) {
config.debug_level = XCE_LOG_DEBUG;
}
/* Performed after argument parsing so -h/--help exits immediately
* without requiring root privileges. */
check_user();
if (logdir == NULL) {
logdir = strdup("/tmp");
}
/* P2.5-5: populate and validate xce_cluster_config_t when --peers given */
if (peers_str) {
if (xce_parse_peers(peers_str, &cluster_cfg) != 0) {
@ -471,23 +483,35 @@ int main(int argc, char* argv[])
"Use -n.\n");
return 1;
}
if (cluster_cfg.candidates_count > 3) {
if (cluster_cfg.candidates_count > N_MAX_MASTERS) {
fprintf(stderr,
"xcecd: WARN: --peers has %d candidates; "
"only <=3 validated for HA\n",
cluster_cfg.candidates_count);
"only <=%d validated for HA\n",
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);
}
/* use_ha and cluster_cfg are consumed after xcecd_start() sets cc_ctx */
/* 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
* is threaded through to cc_ctx->cluster_cfg (via xcecd_ctx) before
* controller_connector_start() fires the very first join. Previously
* this was assigned directly onto cc_ctx AFTER xcecd_start() returned,
* which was already too late -- the first join had already run against
* an empty (zero-initialized) cluster_cfg, silently ignoring --peers on
* startup. When no --peers was given, cluster_cfg stays {0}
* (candidates_count == 0), which is exactly the "no HA list configured"
* case the synthesize-single-candidate path already handles. */
config.cluster_cfg = cluster_cfg;
struct stat st = { 0 };
@ -594,8 +618,10 @@ int main(int argc, char* argv[])
return 1;
}
if (use_ha && cc_ctx != NULL) {
cc_ctx->cluster_cfg = cluster_cfg;
if (use_ha) {
/* cluster_cfg was already threaded into cc_ctx->cluster_cfg before
* xcecd_start() ran (see config.cluster_cfg assignment above) --
* this is confirmation logging only, not the point of assignment. */
XCECD_LOG(XCE_LOG_INFO,
"HA mode active: %d candidates, primary will be auto-discovered",
cluster_cfg.candidates_count);

View file

@ -799,8 +799,12 @@ static int read_memory_info(double* mem_total, double* mem_free,
}
/**
* @brief The mem index gives the currently available physical memory in MB.
* @return A string representing the available physical memory in megabytes.
* @brief The mem index gives the currently available physical memory in
* bytes (canonical internal/wire unit -- same convention as the
* maxmem host attribute; CLI display converts to a human-readable
* unit, config-time thresholds may be written with a unit suffix
* like "500MB" and are converted to bytes when parsed).
* @return A string representing the available physical memory in bytes.
*/
char* node_info_get_mem(void)
{
@ -813,14 +817,14 @@ char* node_info_get_mem(void)
return NULL;
}
// Read memory information
// Read memory information (read_memory_info reports MB)
if (read_memory_info(&mem_total, &mem_free, &mem_used) != 0) {
free(result);
return NULL;
}
// Format the available memory as a string (in MB)
int ret = snprintf(result, 64, "%.2f", mem_free);
// Format the available memory as a string (in bytes)
int ret = snprintf(result, 64, "%.2f", mem_free * 1024.0 * 1024.0);
if (ret < 0 || ret >= 64) {
XCECD_LOG(XCE_LOG_ERROR, "Failed to format memory information string");
free(result);
@ -831,9 +835,11 @@ char* node_info_get_mem(void)
}
/**
* @brief The tmp index is the space available in MB or in units set
* on the file system that contains the temporary directory.
* @return A string representing the available space in temporary directory.
* @brief The tmp index is the space available in bytes (canonical internal/
* wire unit -- see node_info_get_mem) on the file system that
* contains the temporary directory.
* @return A string representing the available space in the temporary
* directory, in bytes.
*/
char* node_info_get_tmp(void)
{
@ -859,11 +865,8 @@ char* node_info_get_tmp(void)
unsigned long long free_bytes
= (unsigned long long)fs_stat.f_bavail * fs_stat.f_frsize;
// Convert to MB
double tmp_free_mb = (double)free_bytes / (1024.0 * 1024.0);
// Format the available tmp space as a string (in MB)
int ret = snprintf(result, 64, "%.2f", tmp_free_mb);
// Format the available tmp space as a string (in bytes)
int ret = snprintf(result, 64, "%llu", free_bytes);
if (ret < 0 || ret >= 64) {
XCECD_LOG(XCE_LOG_ERROR, "Failed to format tmp space string");
free(result);

View file

@ -203,9 +203,29 @@ static xce_resval_t* collect_load_indices(int* count)
return NULL;
}
char hostname[256];
if (gethostname(hostname, sizeof(hostname)) != 0) {
strcpy(hostname, "unknown");
/* bug-1569: this worker's JOIN identity (the -n/--name value, e.g.
* "pc13v" -- xcecd_ctx.virtual_name, set from config->virutal_name in
* xcecd_init(), xcecd.c) is what the controller/scheduler key every
* OTHER per-worker resource value by (mark_host_res()/update_val_res(),
* worker.c; sched_get_worker(), core_events.c). Using gethostname()
* (the raw OS/container hostname, which is NOT the same value and can
* be identical across every worker in a multi-worker-per-host test
* setup, or simply a generic sandbox/container name unrelated to any
* configured worker identity) here instead made every periodic load-
* index report's .host wrong -- the controller forwards it to the
* scheduler unmodified (do_update_resvals(), opupd.c), which then
* fails sched_get_worker(core, rv->host) for the REAL worker and
* silently drops the value ("RESVALS_DELTA: unknown worker <host>,
* skipping", core_events.c) -- live-confirmed: gethostname() here
* returned "xce" in this sandbox, not "pc13v". */
const char* hostname = xcecd_ctx.virtual_name;
char hostname_buf[256];
if (hostname == NULL || hostname[0] == '\0') {
if (gethostname(hostname_buf, sizeof(hostname_buf)) != 0) {
strcpy(hostname_buf, "unknown");
}
hostname = hostname_buf;
}
for (int i = 0; i < rescount; i++) {

View file

@ -62,6 +62,7 @@ static job_client_map_t* job_client_map = NULL;
__attribute__((unused)) static void add_job_client_mapping(uint64_t job_id,
client_t* client)
{
char jobidStr[64];
job_client_map_t* mapping
= (job_client_map_t*)calloc(1, sizeof(job_client_map_t));
if (!mapping)
@ -74,7 +75,8 @@ __attribute__((unused)) static void add_job_client_mapping(uint64_t job_id,
client->current_job_id = job_id;
XCECD_LOG(XCE_LOG_DEBUG, "Added job %lu to client %lu mapping", job_id,
fmt_jobid(jobidStr, sizeof(jobidStr), job_id);
XCECD_LOG(XCE_LOG_DEBUG, "Added job %s to client %lu mapping", jobidStr,
client->id);
}
@ -87,6 +89,7 @@ __attribute__((unused)) static void remove_job_client_mapping(uint64_t job_id)
{
job_client_map_t* current = job_client_map;
job_client_map_t* prev = NULL;
char jobidStr[64];
while (current) {
if (current->job_id == job_id) {
@ -100,8 +103,9 @@ __attribute__((unused)) static void remove_job_client_mapping(uint64_t job_id)
current->client->current_job_id = 0;
}
XCECD_LOG(XCE_LOG_DEBUG, "Removed job %lu from client mapping",
job_id);
fmt_jobid(jobidStr, sizeof(jobidStr), job_id);
XCECD_LOG(XCE_LOG_DEBUG, "Removed job %s from client mapping",
jobidStr);
free(current);
return;
@ -488,28 +492,36 @@ static void on_master_read(uv_stream_t* client, ssize_t nread,
master_status_callback(&msg->data.status);
}
} else if (msg->type == MSG_JOB_SIGNAL_ACK) {
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), msg->data.signal.job_id);
XCECD_LOG(XCE_LOG_DEBUG,
"Received signal acknowledgment for job %lu, signal %d, "
"Received signal acknowledgment for job %s, signal %d, "
"force %d",
msg->data.signal.job_id, msg->data.signal.signal,
jobidStr, msg->data.signal.signal,
msg->data.signal.force);
} else if (msg->type == MSG_JOB_CKPT_ACK) {
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), msg->data.ckpt.job_id);
XCECD_LOG(XCE_LOG_DEBUG,
"Received checkpoint acknowledgment for job %lu, signal "
"Received checkpoint acknowledgment for job %s, signal "
"%d, method %s",
msg->data.ckpt.job_id, msg->data.ckpt.kill,
jobidStr, msg->data.ckpt.kill,
msg->data.ckpt.method);
} else if (msg->type == MSG_JOB_CKPT_RESULT) {
XCECD_LOG(XCE_LOG_DEBUG, "Received checkpoint result for job %lu",
msg->data.ckpt.job_id);
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), msg->data.ckpt.job_id);
XCECD_LOG(XCE_LOG_DEBUG, "Received checkpoint result for job %s",
jobidStr);
if (master_ckpt_result_callback) {
master_ckpt_result_callback(msg->data.ckpt_result.job_id,
msg->data.ckpt_result.status,
msg->data.ckpt_result.restore_cmd);
}
} else if (msg->type == MSG_JOB_RUSAGES) {
XCECD_LOG(XCE_LOG_DEBUG, "Received rusage info for job %lu",
msg->data.rusages.job_id);
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), msg->data.rusages.job_id);
XCECD_LOG(XCE_LOG_DEBUG, "Received rusage info for job %s",
jobidStr);
if (master_rusages_callback) {
// deserialize payload
xce_job_rusages_t* rusages
@ -638,8 +650,10 @@ static void on_worker_read(uv_stream_t* server, ssize_t nread,
worker_id = msg->data.assignment.job_id;
worker_job_callback(&msg->data.assignment);
} else if (msg->type == MSG_JOB_SIGNAL && worker_signal_callback) {
XCECD_WORKER_LOG(XCE_LOG_DEBUG, "Received signal %d for job %lu",
msg->data.signal.signal, msg->data.signal.job_id);
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), msg->data.signal.job_id);
XCECD_WORKER_LOG(XCE_LOG_DEBUG, "Received signal %d for job %s",
msg->data.signal.signal, jobidStr);
worker_signal_callback(msg->data.signal.job_id,
msg->data.signal.signal,
@ -649,9 +663,11 @@ static void on_worker_read(uv_stream_t* server, ssize_t nread,
msg->data.signal.signal,
msg->data.signal.force);
} else if (msg->type == MSG_JOB_CKPT && worker_ckpt_callback) {
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), msg->data.ckpt.job_id);
XCECD_WORKER_LOG(XCE_LOG_DEBUG,
"Received checkpoint request for job %lu",
msg->data.ckpt.job_id);
"Received checkpoint request for job %s",
jobidStr);
worker_ckpt_callback(msg->data.ckpt.job_id, msg->data.ckpt.kill,
msg->data.ckpt.method);
@ -977,6 +993,7 @@ void ipc_worker_send_status(job_runtime_info_t* job_info,
return;
ipc_message_t msg;
char jobidStr[64];
memset(&msg, 0, sizeof(msg));
msg.type = MSG_JOB_STATUS;
msg.data.status.job_id = job_info->job_id;
@ -1020,15 +1037,16 @@ void ipc_worker_send_status(job_runtime_info_t* job_info,
XCECD_WORKER_LOG(XCE_LOG_DEBUG, "Sending job status to master: %s",
status_message);
fmt_jobid(jobidStr, sizeof(jobidStr), job_info->job_id);
if (job_info->job_status == JOB_DONE) {
XCECD_WORKER_LOG(XCE_LOG_INFO, "Job %lu completed successfully.",
job_info->job_id);
XCECD_WORKER_LOG(XCE_LOG_INFO, "Job %s completed successfully.",
jobidStr);
} else if (job_info->job_status == JOB_FAILED) {
XCECD_WORKER_LOG(XCE_LOG_ERROR, "Job %lu failed: %s", job_info->job_id,
XCECD_WORKER_LOG(XCE_LOG_ERROR, "Job %s failed: %s", jobidStr,
status_message);
} else if (job_info->job_status == JOB_EXITED) {
XCECD_WORKER_LOG(XCE_LOG_ERROR, "Job %lu exited with code %d: %s",
job_info->job_id, job_info->exit_code, status_message);
XCECD_WORKER_LOG(XCE_LOG_ERROR, "Job %s exited with code %d: %s",
jobidStr, job_info->exit_code, status_message);
}
}
@ -1232,9 +1250,12 @@ void ipc_worker_set_reconnection_callback(worker_reconnect_callback_t callback)
bool ipc_master_send_job_signal(uint64_t job_id, int signal, bool force)
{
client_t* target_client = find_client_by_job_id(job_id);
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), job_id);
if (!target_client) {
XCECD_LOG(XCE_LOG_ERROR, "No worker found for job %lu", job_id);
XCECD_LOG(XCE_LOG_ERROR, "No worker found for job %s", jobidStr);
return false;
}
@ -1251,12 +1272,12 @@ bool ipc_master_send_job_signal(uint64_t job_id, int signal, bool force)
int r = uv_write(req, (uv_stream_t*)target_client->pipe, &buf, 1,
on_write_complete);
if (r == 0) {
XCECD_LOG(XCE_LOG_DEBUG, "Sent signal %d for job %lu to worker %d",
signal, job_id, target_client->worker_pid);
XCECD_LOG(XCE_LOG_DEBUG, "Sent signal %d for job %s to worker %d",
signal, jobidStr, target_client->worker_pid);
return true;
} else {
XCECD_LOG(XCE_LOG_ERROR, "Failed to send signal %d for job %lu: %s",
signal, job_id, uv_strerror(r));
XCECD_LOG(XCE_LOG_ERROR, "Failed to send signal %d for job %s: %s",
signal, jobidStr, uv_strerror(r));
free(req);
return false;
}
@ -1278,6 +1299,7 @@ void ipc_worker_send_signal_ack(uint64_t job_id, int signal, bool force)
return;
ipc_message_t msg;
char jobidStr[64];
memset(&msg, 0, sizeof(msg));
msg.type = MSG_JOB_SIGNAL_ACK;
msg.data.signal.job_id = job_id;
@ -1298,9 +1320,10 @@ void ipc_worker_send_signal_ack(uint64_t job_id, int signal, bool force)
}
}
fmt_jobid(jobidStr, sizeof(jobidStr), job_id);
XCECD_WORKER_LOG(
XCE_LOG_DEBUG,
"Sent signal acknowledgment for job %lu, signal %d, force %d", job_id,
"Sent signal acknowledgment for job %s, signal %d, force %d", jobidStr,
signal, force);
}
@ -1334,9 +1357,12 @@ void ipc_worker_set_ckpt_callback(job_ckpt_callback_t callback)
bool ipc_master_send_job_ckpt(uint64_t job_id, int kill, const char* method)
{
client_t* target_client = find_client_by_job_id(job_id);
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), job_id);
if (!target_client) {
XCECD_LOG(XCE_LOG_ERROR, "No worker found for job %lu", job_id);
XCECD_LOG(XCE_LOG_ERROR, "No worker found for job %s", jobidStr);
return false;
}
@ -1354,13 +1380,13 @@ bool ipc_master_send_job_ckpt(uint64_t job_id, int kill, const char* method)
on_write_complete);
if (r == 0) {
XCECD_LOG(XCE_LOG_DEBUG,
"Sent checkpoint request for job %lu to worker %d",
job_id, target_client->worker_pid);
"Sent checkpoint request for job %s to worker %d",
jobidStr, target_client->worker_pid);
return true;
} else {
XCECD_LOG(XCE_LOG_ERROR,
"Failed to send checkpoint request for job %lu: %s",
job_id, uv_strerror(r));
"Failed to send checkpoint request for job %s: %s",
jobidStr, uv_strerror(r));
free(req);
return false;
}
@ -1382,6 +1408,7 @@ bool ipc_worker_send_ckpt_ack(uint64_t job_id, int kill, const char* method)
return false;
ipc_message_t msg;
char jobidStr[64];
memset(&msg, 0, sizeof(msg));
msg.type = MSG_JOB_CKPT;
msg.data.ckpt.job_id = job_id;
@ -1401,7 +1428,8 @@ bool ipc_worker_send_ckpt_ack(uint64_t job_id, int kill, const char* method)
}
}
XCECD_WORKER_LOG(XCE_LOG_DEBUG, "Sent job checkpoint for job %lu", job_id);
fmt_jobid(jobidStr, sizeof(jobidStr), job_id);
XCECD_WORKER_LOG(XCE_LOG_DEBUG, "Sent job checkpoint for job %s", jobidStr);
return true;
}
@ -1440,6 +1468,7 @@ bool ipc_worker_send_ckpt_result(uint64_t job_id, int status,
return false;
ipc_message_t msg;
char jobidStr[64];
memset(&msg, 0, sizeof(msg));
msg.type = MSG_JOB_CKPT_RESULT;
msg.data.ckpt_result.job_id = job_id;
@ -1461,8 +1490,9 @@ bool ipc_worker_send_ckpt_result(uint64_t job_id, int status,
}
}
XCECD_WORKER_LOG(XCE_LOG_DEBUG, "Sent job checkpoint result for job %lu",
job_id);
fmt_jobid(jobidStr, sizeof(jobidStr), job_id);
XCECD_WORKER_LOG(XCE_LOG_DEBUG, "Sent job checkpoint result for job %s",
jobidStr);
return true;
}
@ -1478,6 +1508,7 @@ bool ipc_worker_send_rusages(xce_job_rusages_t* rusages)
return false;
ipc_message_t msg;
char jobidStr[64];
memset(&msg, 0, sizeof(msg));
msg.type = MSG_JOB_RUSAGES;
msg.data.rusages.job_id = rusages->jobid;
@ -1538,7 +1569,7 @@ bool ipc_worker_send_rusages(xce_job_rusages_t* rusages)
}
}
XCECD_WORKER_LOG(XCE_LOG_DEBUG, "Sent rusage info for job %lu",
rusages->jobid);
fmt_jobid(jobidStr, sizeof(jobidStr), rusages->jobid);
XCECD_WORKER_LOG(XCE_LOG_DEBUG, "Sent rusage info for job %s", jobidStr);
return true;
}

View file

@ -34,6 +34,7 @@ void xcecd_process_command(void* arg)
{
pid_t pid = 0;
char unit_name[256];
char jobidStr[64];
fork_task_t* ctx = (fork_task_t*)arg;
if (!ctx || !ctx->startinfo || !ctx->startinfo->cmdline) {
@ -57,8 +58,9 @@ void xcecd_process_command(void* arg)
if (!ctx->startinfo) {
xcecd_job_start_error(ctx->xce_jobid);
XCECD_LOG(XCE_LOG_FATAL, "No startinfo provided, job %lu start failed.",
ctx->xce_jobid);
fmt_jobid(jobidStr, sizeof(jobidStr), ctx->xce_jobid);
XCECD_LOG(XCE_LOG_FATAL, "No startinfo provided, job %s start failed.",
jobidStr);
return;
}
@ -328,6 +330,11 @@ controller_connector_init(xcecd_context_t* ctx)
cc_ctx->thread_pool = ctx->thread_pool;
cc_ctx->process_manager = ctx->process_manager;
cc_ctx->worker_list = (void**)&ctx->worker_list;
/* component-failover-adoption task 2.2 (Req 1.4): populate the full
* candidate list here, before controller_connector_start() (called
* right after this returns, still inside xcecd_start()) fires the very
* first join -- so the first connect attempt already sees it. */
cc_ctx->cluster_cfg = ctx->cluster_cfg;
return cc_ctx;
}
@ -343,8 +350,12 @@ static void init_host_attrs(void)
= hardware_monitor_get_total_memory(xcecd_ctx.hardware_monitor);
}
host_attrs.maxswap = 1024;
host_attrs.maxtmp = 2048;
/* Placeholder stub values (bug-1554, real swap/tmp-disk detection not
* yet implemented) -- kept as bytes to match every other memory-scale
* host attribute's canonical unit (maxmem, mem, tmp). Magnitude chosen
* to match the pre-existing MB-scale placeholders (1024MB/2048MB). */
host_attrs.maxswap = 1024.0 * 1024.0 * 1024.0;
host_attrs.maxtmp = 2048.0 * 1024.0 * 1024.0;
host_attrs.ndisks = 1;
host_attrs.hostmodel = strdup("Intel64");
@ -384,6 +395,11 @@ int xcecd_init(const xcecd_config_t* config)
xcecd_ctx.update_interval = config->update_interval;
xcecd_ctx.join_interval = config->join_interval;
xcecd_ctx.debug_level = config->debug_level;
/* component-failover-adoption task 2.2 (Req 1.4): thread the parsed
* --peers candidate list through so it reaches cc_ctx before the first
* join, instead of main.c assigning cc_ctx->cluster_cfg after
* xcecd_start() already ran. */
xcecd_ctx.cluster_cfg = config->cluster_cfg;
/* thread pool initializing */
xcecd_ctx.thread_pool = thread_pool_create(config->thread_pool_size);

View file

@ -1384,6 +1384,7 @@ xce_job_rusages_t* collect_job_rusages(uint64_t job_id)
int r;
char unit_name[256];
char unit_path[512];
char jobidStr[64];
xce_job_rusages_t* rusages = NULL;
rusages = (xce_job_rusages_t*)calloc(1, sizeof(xce_job_rusages_t));
@ -1699,10 +1700,11 @@ xce_job_rusages_t* collect_job_rusages(uint64_t job_id)
strcpy(pgids_str, "none");
}
fmt_jobid(jobidStr, sizeof(jobidStr), job_id);
XCECD_LOG(XCE_LOG_DEBUG3,
"Job %lu rusage: CPU: %.2fs, Mem: %.2fMB, Swap: %.2fMB, Tasks: "
"Job %s rusage: CPU: %.2fs, Mem: %.2fMB, Swap: %.2fMB, Tasks: "
"%d, PIDs: %d [%s], PGIDs: %d [%s]",
job_id, rusages->usages[0]->value, rusages->usages[1]->value,
jobidStr, rusages->usages[0]->value, rusages->usages[1]->value,
rusages->usages[2]->value, (int)rusages->usages[3]->value,
rusages->npids, pids_str, rusages->npgids, pgids_str);
#endif

View file

@ -1,239 +0,0 @@
#include "xcecd_conf.h"
#include "xcelog.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const cyaml_schema_value_t management_host_schema = {
.type = CYAML_STRING,
.flags = CYAML_FLAG_POINTER,
.data_size = sizeof(char*),
.string = {
.min = 0,
.max = MAX_HOSTNAME_LEN,
},
};
static const cyaml_schema_field_t worker_config_fields_schema[] = {
{
.key = "management_list",
.data_offset = offsetof(worker_config_t, management_list),
.value = {
.type = CYAML_SEQUENCE,
.flags = CYAML_FLAG_POINTER,
.data_size = sizeof(char*),
.sequence = {
.entry = &management_host_schema,
.min = 0,
.max = CYAML_UNLIMITED,
},
},
.count_offset = offsetof(worker_config_t, management_list_count),
.count_size = sizeof(uint32_t),
},
{
.key = "controller_port",
.data_offset = offsetof(worker_config_t, controller_port),
.value = {
.type = CYAML_UINT,
.flags = CYAML_FLAG_DEFAULT,
.data_size = sizeof(uint16_t),
},
},
{
.key = "log_level",
.data_offset = offsetof(worker_config_t, log_level),
.value = {
.type = CYAML_STRING,
.flags = CYAML_FLAG_POINTER,
.data_size = sizeof(char*),
.string = {
.min = 0,
.max = MAX_LOG_LEVEL_LEN,
},
},
},
{
.key = "log_dir",
.data_offset = offsetof(worker_config_t, log_dir),
.value = {
.type = CYAML_STRING,
.flags = CYAML_FLAG_POINTER,
.data_size = sizeof(char*),
.string = {
.min = 0,
.max = MAX_PATH_LEN,
},
},
},
{ .key = NULL }
};
static const cyaml_schema_field_t root_fields_schema[] = {
{
.key = "workerconf",
.value = {
.type = CYAML_MAPPING,
.flags = CYAML_FLAG_DEFAULT,
.data_size = sizeof(worker_config_t),
.mapping = {worker_config_fields_schema},
},
},
CYAML_FIELD_END
};
static const cyaml_schema_value_t root_schema = {
.type = CYAML_MAPPING,
.flags = CYAML_FLAG_POINTER,
.data_size = sizeof(worker_config_t),
.mapping = { root_fields_schema },
};
static const cyaml_config_t cyaml_config = {
.log_fn = cyaml_log,
.mem_fn = cyaml_mem,
.log_level = CYAML_LOG_WARNING,
};
config_result_t* config_load(const char* config_path)
{
config_result_t* result = calloc(1, sizeof(config_result_t));
if (!result) {
return NULL;
}
result->error_code = CONFIG_SUCCESS;
result->config = NULL;
FILE* file = fopen(config_path, "r");
if (!file) {
result->error_code = CONFIG_ERROR_FILE_NOT_FOUND;
snprintf(result->error_message, sizeof(result->error_message),
"Cannot open config file '%s': %s", config_path,
strerror(errno));
return result;
}
fclose(file);
cyaml_err_t err = cyaml_load_file(config_path, &cyaml_config, &root_schema,
(void**)&result->config, NULL);
if (err != CYAML_OK) {
result->error_code = CONFIG_ERROR_PARSE_FAILED;
snprintf(result->error_message, sizeof(result->error_message),
"YAML parse error: %s", cyaml_strerror(err));
return result;
}
if (!result->config) {
result->error_code = CONFIG_ERROR_INVALID_DATA;
snprintf(result->error_message, sizeof(result->error_message),
"Configuration data is null");
return result;
}
if (result->config->controller_port == 0) {
result->error_code = CONFIG_ERROR_INVALID_DATA;
snprintf(result->error_message, sizeof(result->error_message),
"controller_port must be greater than 0");
return result;
}
if (!result->config->log_level || strlen(result->config->log_level) == 0) {
result->error_code = CONFIG_ERROR_INVALID_DATA;
snprintf(result->error_message, sizeof(result->error_message),
"log_level cannot be empty");
return result;
}
if (!result->config->log_dir || strlen(result->config->log_dir) == 0) {
result->error_code = CONFIG_ERROR_INVALID_DATA;
snprintf(result->error_message, sizeof(result->error_message),
"log_dir cannot be empty");
return result;
}
return result;
}
config_result_t* config_load_default(void)
{
return config_load(DEFAULT_CONFIG_PATH);
}
void config_free(config_result_t* result)
{
if (!result)
return;
if (result->config) {
cyaml_free(&cyaml_config, &root_schema, result->config, 0);
}
free(result);
}
const char* config_error_string(config_error_t error)
{
switch (error) {
case CONFIG_SUCCESS:
return "Success";
case CONFIG_ERROR_FILE_NOT_FOUND:
return "Configuration file not found";
case CONFIG_ERROR_PARSE_FAILED:
return "YAML parsing failed";
case CONFIG_ERROR_INVALID_DATA:
return "Invalid configuration data";
case CONFIG_ERROR_MEMORY:
return "Memory allocation error";
case CONFIG_ERROR_UNKNOWN:
default:
return "Unknown error";
}
}
int level_to_int(const char* level)
{
if (!level)
return XCE_LOG_INFO; // Default to INFO if NULL
if (strcmp(level, "debug3") == 0) {
return XCE_LOG_DEBUG3;
} else if (strcmp(level, "debug2") == 0) {
return XCE_LOG_DEBUG2;
} else if (strcmp(level, "debug") == 0) {
return XCE_LOG_DEBUG;
} else if (strcmp(level, "notice") == 0) {
return XCE_LOG_NOTICE;
} else if (strcmp(level, "info") == 0) {
return XCE_LOG_INFO;
} else if (strcmp(level, "warn") == 0) {
return XCE_LOG_WARN;
} else if (strcmp(level, "error") == 0) {
return XCE_LOG_ERROR;
} else if (strcmp(level, "fatal") == 0) {
return XCE_LOG_FATAL;
} else if (strcmp(level, "off") == 0) {
return XCE_LOG_OFF;
} else {
return XCE_LOG_INFO; // Default to INFO for unknown levels
}
}
void config_print(const worker_config_t* config)
{
if (!config) {
printf("Configuration is NULL\n");
return;
}
printf("Worker Configuration:\n");
printf(" Controller Port: %u\n", config->controller_port);
printf(" Log Level: %s\n", config->log_level);
printf(" Log Directory: %s\n", config->log_dir);
printf(" Management List (%u hosts):\n", config->management_list_count);
for (uint32_t i = 0; i < config->management_list_count; i++) {
printf(" - %s\n", config->management_list[i].hostname);
}
}

View file

@ -188,6 +188,25 @@ int dbus_workers_update(xcecd_worker_t** worker_list, xcecd_job_t* job,
xcecd_worker_t* worker = *worker_list;
while (worker) {
/* bug-1589: capture the NEXT node before doing any work on this one.
* Every early "nothing more to do for this worker on this pass"
* exit below must advance to `next` and CONTINUE scanning the rest
* of the list -- not silently stay stuck on this same node (several
* branches below used a bare `continue`, which re-tests the SAME
* `worker` pointer forever) and not RETURN out of the whole function
* (the old `worker->worker_status == WORKER_DONE` branch did
* `return true` -- true == 1 == XCECD_ERROR, and worse, it aborted
* the scan of every worker after the first DONE/EXITED one it saw).
* Net effect on a virtual/real worker with N concurrently-dispatched
* jobs in worker_list: only the head of the list ever got processed
* -- job [0] of a job array got its on_job_start report, jobs
* [1..N-1] were starved forever (until job [0] finished AND got
* reaped by cleanup_worker_list(), at which point the NEXT worker
* became the new head and hit the identical bug). Capturing `next`
* up front and always advancing to it is the general fix for every
* branch in this loop, real-mode and VIRTUAL_CLUSTER alike. */
xcecd_worker_t* next = worker->next;
#ifdef VIRTUAL_CLUSTER
if (worker->worker_status == WORKER_RUNNING
&& worker->job_status == JOB_STARTING) {
@ -198,14 +217,15 @@ int dbus_workers_update(xcecd_worker_t** worker_list, xcecd_job_t* job,
ret = update_worker_status(*worker_list, worker->worker_pid,
WORKER_RUNNING, job, db);
worker = next;
continue;
}
#endif
if (worker->worker_status == WORKER_DONE
|| worker->worker_status == WORKER_EXITED) {
worker = worker->next;
return true;
worker = next;
continue;
}
char unit_name[256];
@ -222,6 +242,7 @@ int dbus_workers_update(xcecd_worker_t** worker_list, xcecd_job_t* job,
if (worker->job_status == JOB_STARTING) {
worker->job_status = JOB_RUNNING;
worker->worker_status = WORKER_RUNNING;
worker = next;
continue;
}
@ -238,6 +259,7 @@ int dbus_workers_update(xcecd_worker_t** worker_list, xcecd_job_t* job,
ret = update_worker_status(*worker_list, worker->worker_pid,
WORKER_EXITED, job, db);
}
worker = next;
continue;
#endif
XCECD_LOG(XCE_LOG_DEBUG, "Worker %d is done",
@ -265,6 +287,7 @@ int dbus_workers_update(xcecd_worker_t** worker_list, xcecd_job_t* job,
ret = update_worker_status(*worker_list, worker->worker_pid,
WORKER_DONE, job, db);
}
worker = next;
continue;
#endif
XCECD_LOG(XCE_LOG_WARN, "Failed to get worker exit code");
@ -288,6 +311,7 @@ int dbus_workers_update(xcecd_worker_t** worker_list, xcecd_job_t* job,
if (worker->job_status == JOB_STARTING) {
worker->job_status = JOB_RUNNING;
worker->worker_status = WORKER_RUNNING;
worker = next;
continue;
}
@ -329,7 +353,7 @@ int dbus_workers_update(xcecd_worker_t** worker_list, xcecd_job_t* job,
// worker->worker_pid);
}
}
worker = worker->next;
worker = next;
}
return ret;
@ -384,6 +408,7 @@ void dbus_stop_all_workers(xcecd_worker_t** worker_list, sqlite3* db)
xcecd_worker_t* worker = *worker_list;
while (worker) {
char unit_name[256];
char jobidStr[64];
make_job_sd_unit_name(unit_name, sizeof(unit_name) - 1, "xcecd-worker",
worker->job_info.jobid);
@ -396,7 +421,8 @@ void dbus_stop_all_workers(xcecd_worker_t** worker_list, sqlite3* db)
update_worker_status(*worker_list, worker->worker_pid, WORKER_EXITED,
&job, db);
XCECD_LOG(XCE_LOG_WARN, "Job %lu killed.", worker->job_info.jobid);
fmt_jobid(jobidStr, sizeof(jobidStr), worker->job_info.jobid);
XCECD_LOG(XCE_LOG_WARN, "Job %s killed.", jobidStr);
worker = worker->next;
}

View file

@ -80,7 +80,10 @@ static void free_xce_job_rusages(xce_job_rusages_t* rusages)
static void on_rusage_timer(uv_timer_t* timer)
{
if (jobid > 0) {
XCECD_WORKER_LOG(XCE_LOG_DEBUG, "Collecting rusage for job %lu", jobid);
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), jobid);
XCECD_WORKER_LOG(XCE_LOG_DEBUG, "Collecting rusage for job %s",
jobidStr);
xce_job_rusages_t* rusages = collect_job_rusages(jobid);
@ -589,8 +592,10 @@ static void check_and_reconnect()
static void xjob_start_vnc()
{
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), jobid);
XCECD_WORKER_LOG(XCE_LOG_DEBUG3,
"Starting VNC server for interactive job %lu", jobid);
"Starting VNC server for interactive job %s", jobidStr);
char vnc_start_cmd[MAXLINELEN] = { 0 };
if (access(XJOB_PRE_EXEC, F_OK) != 0) {
@ -652,13 +657,16 @@ static void xjob_start_vnc()
elapsed_time++;
}
XCECD_WORKER_LOG(XCE_LOG_DEBUG3, "VNC server started for job %lu", jobid);
XCECD_WORKER_LOG(XCE_LOG_DEBUG3, "VNC server started for job %s",
jobidStr);
}
static void xjob_stop_vnc()
{
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), jobid);
XCECD_WORKER_LOG(XCE_LOG_DEBUG3,
"Stopping VNC server for interactive job %lu", jobid);
"Stopping VNC server for interactive job %s", jobidStr);
char vnc_stop_cmd[MAXLINELEN] = { 0 };
if (access(XJOB_POST_EXEC, F_OK) != 0) {
@ -686,7 +694,8 @@ static void xjob_stop_vnc()
exit(1);
}
XCECD_WORKER_LOG(XCE_LOG_DEBUG3, "VNC server stopped for job %lu", jobid);
XCECD_WORKER_LOG(XCE_LOG_DEBUG3, "VNC server stopped for job %s",
jobidStr);
}
static void worker_job_callback(job_assignment_t* info)
@ -695,6 +704,7 @@ static void worker_job_callback(job_assignment_t* info)
char* url = NULL;
char* start_exdata_info = NULL;
int rc = 0;
char jobidStr[64];
if (!info) {
XCECD_WORKER_LOG(XCE_LOG_ERROR, "Received null job assignment");
@ -715,6 +725,8 @@ static void worker_job_callback(job_assignment_t* info)
uv_proc_manager.job_id = jobid;
job_runtime_info.job_id = jobid;
fmt_jobid(jobidStr, sizeof(jobidStr), jobid);
if (info->q_pre_exec[0]) {
strncpy(job_runtime_info.q_pre_exec, info->q_pre_exec,
sizeof(job_runtime_info.q_pre_exec) - 1);
@ -735,8 +747,8 @@ static void worker_job_callback(job_assignment_t* info)
sizeof(job_runtime_info.job_post_exec) - 1);
}
XCECD_WORKER_LOG(XCE_LOG_INFO, "Received job: %s, job_id: %lu, exdata: %s",
info->cmd, jobid,
XCECD_WORKER_LOG(XCE_LOG_INFO, "Received job: %s, job_id: %s, exdata: %s",
info->cmd, jobidStr,
info->exdata[0] ? info->exdata : "no data");
XCECD_WORKER_LOG(XCE_LOG_DEBUG3, "Job pre-exec: %s",
@ -761,21 +773,21 @@ static void worker_job_callback(job_assignment_t* info)
if (!parse_exdata(info->exdata, &mydata.env_vars, mydata.transfer_info,
jobid)) {
XCECD_WORKER_LOG(XCE_LOG_ERROR, "Failed to parse exdata of job %lu",
jobid);
XCECD_WORKER_LOG(XCE_LOG_ERROR, "Failed to parse exdata of job %s",
jobidStr);
exit(1);
}
XCECD_WORKER_LOG(XCE_LOG_DEBUG3,
"Parsed exdata for job %lu: interactive=%d", jobid,
"Parsed exdata for job %s: interactive=%d", jobidStr,
mydata.transfer_info->interactive_mode);
int job_port
= check_port(port_info, mydata.transfer_info->interactive_mode);
if (job_port < 0) {
XCECD_WORKER_LOG(XCE_LOG_ERROR,
"Failed to check port availability for job %lu",
jobid);
"Failed to check port availability for job %s",
jobidStr);
exit(1);
} else if (job_port > 5900 && job_port < 10000) {
if (mydata.transfer_info->interactive_mode == TTY
@ -784,13 +796,13 @@ static void worker_job_callback(job_assignment_t* info)
} else if (mydata.transfer_info->interactive_mode == XJOB) {
port_info.xjob_port = job_port;
}
XCECD_WORKER_LOG(XCE_LOG_DEBUG3, "Assigned port %d for job %lu",
job_port, jobid);
XCECD_WORKER_LOG(XCE_LOG_DEBUG3, "Assigned port %d for job %s",
job_port, jobidStr);
} else if (job_port == 0) {
/* normal jobs don't need port info */
} else {
XCECD_WORKER_LOG(XCE_LOG_ERROR, "Invalid port %d for job %lu", job_port,
jobid);
XCECD_WORKER_LOG(XCE_LOG_ERROR, "Invalid port %d for job %s", job_port,
jobidStr);
exit(1);
}
@ -842,7 +854,8 @@ static void worker_job_callback(job_assignment_t* info)
mydata.transfer_info->interactive_mode);
if (url) {
XCECD_WORKER_LOG(XCE_LOG_DEBUG3,
"URL for interactive job %lu: %s", jobid, url);
"URL for interactive job %s: %s", jobidStr,
url);
}
start_exdata = calloc(1, sizeof(start_exdata_t));
@ -858,12 +871,13 @@ static void worker_job_callback(job_assignment_t* info)
if (!start_exdata_info) {
XCECD_WORKER_LOG(
XCE_LOG_ERROR,
"Failed to generate start_exdata_info for job %lu", jobid);
"Failed to generate start_exdata_info for job %s",
jobidStr);
exit(1);
} else {
XCECD_WORKER_LOG(XCE_LOG_DEBUG3,
"Generated start_exdata_info for job %lu: %s",
jobid, start_exdata_info);
"Generated start_exdata_info for job %s: %s",
jobidStr, start_exdata_info);
}
strncpy(job_runtime_info.exdata, start_exdata_info,
@ -879,7 +893,7 @@ static void worker_job_callback(job_assignment_t* info)
ipc_worker_send_status(&job_runtime_info, "Running");
XCECD_WORKER_LOG(XCE_LOG_DEBUG,
"Reported job %lu run status to master.", jobid);
"Reported job %s run status to master.", jobidStr);
FREEUP(job_cmd);
FREEUP(url);
@ -898,7 +912,8 @@ static void worker_job_callback(job_assignment_t* info)
ipc_worker_send_status(&job_runtime_info, "Failed to start process");
XCECD_WORKER_LOG(XCE_LOG_DEBUG,
"Reported job %lu failed status to master.", jobid);
"Reported job %s failed status to master.",
jobidStr);
worker_exit_flag = 1;
return_code = 1;
}
@ -1074,13 +1089,18 @@ static bool check_process_status(pid_t target_pid, int expected_signal,
*/
static void worker_signal_callback(uint64_t job_id, int signal, bool force)
{
XCECD_WORKER_LOG(XCE_LOG_INFO, "Received signal %d for job %lu, force=%d",
signal, job_id, force);
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), job_id);
XCECD_WORKER_LOG(XCE_LOG_INFO, "Received signal %d for job %s, force=%d",
signal, jobidStr, force);
if (job_id != jobid) {
char curJobidStr[64];
fmt_jobid(curJobidStr, sizeof(curJobidStr), jobid);
XCECD_WORKER_LOG(XCE_LOG_WARN,
"Signal for job %lu but current job is %lu", job_id,
jobid);
"Signal for job %s but current job is %s", jobidStr,
curJobidStr);
return;
}
@ -1095,8 +1115,8 @@ static void worker_signal_callback(uint64_t job_id, int signal, bool force)
switch (signal) {
case SIGTERM:
case SIGKILL:
XCECD_WORKER_LOG(XCE_LOG_INFO, "Terminating job %lu with signal %d",
job_id, signal);
XCECD_WORKER_LOG(XCE_LOG_INFO, "Terminating job %s with signal %d",
jobidStr, signal);
if (force) {
success = signal_gid_except_self(SIGKILL);
} else {
@ -1106,36 +1126,36 @@ static void worker_signal_callback(uint64_t job_id, int signal, bool force)
case SIGSTOP:
case SIGTSTP:
XCECD_WORKER_LOG(XCE_LOG_INFO, "Pausing job %lu", job_id);
XCECD_WORKER_LOG(XCE_LOG_INFO, "Pausing job %s", jobidStr);
success = signal_gid_except_self(SIGSTOP);
if (success) {
XCECD_WORKER_LOG(XCE_LOG_DEBUG2, "Job %lu paused successfully",
job_id);
XCECD_WORKER_LOG(XCE_LOG_DEBUG2, "Job %s paused successfully",
jobidStr);
job_runtime_info.job_status = JOB_SUSPENDED;
ipc_worker_send_status(&job_runtime_info, "Suspended");
} else {
XCECD_WORKER_LOG(XCE_LOG_WARN, "Failed to pause job %lu", job_id);
XCECD_WORKER_LOG(XCE_LOG_WARN, "Failed to pause job %s", jobidStr);
}
break;
case SIGCONT:
XCECD_WORKER_LOG(XCE_LOG_INFO, "Resuming job %lu", job_id);
XCECD_WORKER_LOG(XCE_LOG_INFO, "Resuming job %s", jobidStr);
success = signal_gid_except_self(SIGCONT);
if (success) {
XCECD_WORKER_LOG(XCE_LOG_DEBUG2, "Job %lu resumed successfully",
job_id);
XCECD_WORKER_LOG(XCE_LOG_DEBUG2, "Job %s resumed successfully",
jobidStr);
job_runtime_info.job_status = JOB_RUNNING;
ipc_worker_send_status(&job_runtime_info, "Resumed");
} else {
XCECD_WORKER_LOG(XCE_LOG_WARN, "Failed to resume job %lu", job_id);
XCECD_WORKER_LOG(XCE_LOG_WARN, "Failed to resume job %s", jobidStr);
}
break;
default:
XCECD_WORKER_LOG(XCE_LOG_INFO, "Sending signal %d for job %lu", signal,
job_id);
XCECD_WORKER_LOG(XCE_LOG_INFO, "Sending signal %d for job %s", signal,
jobidStr);
success = signal_gid_except_self(signal);
break;
}
@ -1169,12 +1189,14 @@ static char* criu_path()
static void do_criu_ckpt(uint64_t job_id, int kill)
{
pid_t target_pid = 0;
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), job_id);
char* path = criu_path();
if (!path) {
XCECD_WORKER_LOG(XCE_LOG_ERROR,
"CRIU is not available for checkpointing job %lu",
job_id);
"CRIU is not available for checkpointing job %s",
jobidStr);
ipc_worker_send_ckpt_result(job_id, 1, NULL);
return;
@ -1275,9 +1297,9 @@ static void do_criu_ckpt(uint64_t job_id, int kill)
}
fclose(log_file);
if (has_error) {
XCECD_WORKER_LOG(XCE_LOG_ERROR,
"CRIU dump failed for job %lu, check %s for details",
job_id, dump_log);
XCECD_WORKER_LOG(XCE_LOG_ERROR,
"CRIU dump failed for job %s, check %s for details",
jobidStr, dump_log);
ipc_worker_send_ckpt_result(job_id, 1, NULL);
FREEUP(path);
return;
@ -1299,7 +1321,10 @@ static void do_criu_ckpt(uint64_t job_id, int kill)
static void worker_ckpt_callback(uint64_t job_id, int kill, const char* method)
{
XCECD_WORKER_LOG(XCE_LOG_INFO, "Checkpointing job %lu", job_id);
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), job_id);
XCECD_WORKER_LOG(XCE_LOG_INFO, "Checkpointing job %s", jobidStr);
job_runtime_info.job_id = job_id;
@ -1311,7 +1336,8 @@ static void worker_ckpt_callback(uint64_t job_id, int kill, const char* method)
}
XCECD_WORKER_LOG(XCE_LOG_DEBUG,
"Reported job %lu checkpoint status to master.", job_id);
"Reported job %s checkpoint status to master.",
jobidStr);
}
int main(int argc, char* argv[])

View file

@ -199,7 +199,8 @@ void remove_worker(xcecd_worker_t** worker_list, int worker_pid)
int update_worker_status(xcecd_worker_t* worker_list, int worker_pid,
int worker_status, xcecd_job_t* job, sqlite3* db)
{
int ret = XCECD_OK;
int ret = XCECD_OK;
char jobidStr[64];
if (!worker_list) {
XCECD_LOG(XCE_LOG_DEBUG, "Worker list is empty");
@ -227,6 +228,7 @@ int update_worker_status(xcecd_worker_t* worker_list, int worker_pid,
worker->job_info.pidinfo.pids[0] = (unsigned int)job->job_pid;
}
job->job_id = worker->job_info.jobid;
fmt_jobid(jobidStr, sizeof(jobidStr), job->job_id);
if (job->job_status == JOB_DONE || job->job_status == JOB_EXITED) {
worker->job_status = job->job_status;
@ -238,8 +240,8 @@ int update_worker_status(xcecd_worker_t* worker_list, int worker_pid,
&& (job->job_status == JOB_RUNNING)) {
XCECD_LOG(
XCE_LOG_DEBUG,
"Job ID %lu running - sending status to controller",
job->job_id);
"Job ID %s running - sending status to controller",
jobidStr);
worker->job_status = JOB_RUNNING;
xcecd_reply_startjob(job);
@ -253,8 +255,8 @@ int update_worker_status(xcecd_worker_t* worker_list, int worker_pid,
}
} else if (job->job_status == JOB_SUSPENDED) {
// Send controller that job is stopped
XCECD_LOG(XCE_LOG_DEBUG, "Job %lu is suspended.",
job->job_id);
XCECD_LOG(XCE_LOG_DEBUG, "Job %s is suspended.",
jobidStr);
worker->job_status = JOB_SUSPENDED;
xcecd_job_stopped(job->job_id);
@ -269,8 +271,8 @@ int update_worker_status(xcecd_worker_t* worker_list, int worker_pid,
}
} else if (worker->job_status == JOB_SUSPENDED
&& job->job_status == JOB_RUNNING) {
XCECD_LOG(XCE_LOG_DEBUG, "Job %lu is resumed.",
job->job_id);
XCECD_LOG(XCE_LOG_DEBUG, "Job %s is resumed.",
jobidStr);
worker->job_status = JOB_RUNNING;
xcecd_job_resumed(job->job_id);
@ -293,8 +295,8 @@ int update_worker_status(xcecd_worker_t* worker_list, int worker_pid,
worker->worker_status = WORKER_DONE;
worker->job_status = JOB_DONE;
XCECD_LOG(XCE_LOG_DEBUG,
"Job %lu done and sending status to controller",
job->job_id);
"Job %s done and sending status to controller",
jobidStr);
xcecd_job_finished(job);
/* FIXME: add error handling when msg send failure */
@ -313,8 +315,8 @@ int update_worker_status(xcecd_worker_t* worker_list, int worker_pid,
worker->worker_status = WORKER_EXITED;
worker->job_status = JOB_EXITED;
XCECD_LOG(XCE_LOG_DEBUG,
"Job %lu exited and sending status to controller",
job->job_id);
"Job %s exited and sending status to controller",
jobidStr);
xcecd_job_finished(job);
if (db
@ -332,9 +334,9 @@ int update_worker_status(xcecd_worker_t* worker_list, int worker_pid,
worker->worker_status = WORKER_UNKNOWN;
worker->job_status = JOB_EXITED;
XCECD_LOG(XCE_LOG_DEBUG,
"Job %lu status is unknown and sending status to "
"Job %s status is unknown and sending status to "
"controller",
job->job_id);
jobidStr);
xcecd_job_finished(job);
/* FIXME: we don't have unknown status currently */
@ -425,8 +427,11 @@ xcecd_worker_t* find_worker_by_pid(void* worker_list, int pid)
xcecd_worker_t* find_worker_by_jobid(xcecd_worker_t* worker_list,
uint64_t jobid)
{
XCECD_LOG(XCE_LOG_DEBUG, "Finding worker with job ID %lu in worker list",
jobid);
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), jobid);
XCECD_LOG(XCE_LOG_DEBUG, "Finding worker with job ID %s in worker list",
jobidStr);
xcecd_worker_t* worker = worker_list;
while (worker) {
@ -455,8 +460,11 @@ bool resend_job_status(xcecd_worker_t* worker_list)
xcecd_worker_t* worker = worker_list;
while (worker) {
if (worker->worker_status == WORKER_RUNNING) {
XCECD_LOG(XCE_LOG_DEBUG, "Resending job %lu status to controller.",
worker->job_info.jobid);
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), worker->job_info.jobid);
XCECD_LOG(XCE_LOG_DEBUG, "Resending job %s status to controller.",
jobidStr);
xcecd_job_t job;
memset(&job, 0, sizeof(job));
@ -512,11 +520,14 @@ bool check_worker_sleep_time_elapsed(xcecd_worker_t* worker)
double time_diff = difftime(current_time, worker->start_time);
if (time_diff >= worker->sleep_time) {
char jobidStr[64];
fmt_jobid(jobidStr, sizeof(jobidStr), worker->job_info.jobid);
XCECD_LOG(
XCE_LOG_DEBUG,
"Sleep time %.0f >= %d seconds elapsed for worker %d (job %lu)",
"Sleep time %.0f >= %d seconds elapsed for worker %d (job %s)",
time_diff, worker->sleep_time, worker->worker_pid,
worker->job_info.jobid);
jobidStr);
return true;
} else {
// XCECD_LOG(XCE_LOG_TRACE, "Sleep time remaining for worker %d: %.0f

View file

@ -0,0 +1,79 @@
# Worker unit test suite (component-failover-adoption, task 2.1). White-box
# tests #include the production .c file(s) under test directly (same pattern
# as scheduler/testsuite's test_handlers.c etc.), so they link the same
# dependency set the real xcecd/xcecd_worker executables use rather than a
# subset of xcecd_srcs -- see test_controller_connector.c's header comment
# for why controller_connector.c is not also listed in xcecd_srcs here (it
# would double-define every symbol in that file).
worker_test_incdir = include_directories('../include', '../../include', '.',
'../src', '../../lib')
worker_test_deps = [xcecd_worker_deps, unity_dep]
controller_connector_test = executable('controller_connector_test',
'test_controller_connector.c',
c_args: ['-DDBUS_MANAGER',
'-DXCE_CONTROLLER_CONNECTOR_C_PATH="'
+ (meson.current_source_dir() / '..' / 'src' / 'controller_connector.c')
+ '"'],
include_directories: [worker_test_incdir],
dependencies: worker_test_deps,
install: false)
test('worker controller_connector test', controller_connector_test,
suite: 'worker',
timeout: 60,
env: ['ASAN_OPTIONS=detect_leaks=0'])
# bug-1569: collect_load_indices() (resource_sync.c) must tag every periodic
# resource report with xcecd_ctx.virtual_name (the worker's own join
# identity), not gethostname()'s raw OS/container hostname -- see
# test_resource_sync.c's own header comment for the full mechanism trace.
resource_sync_test = executable('resource_sync_test',
'test_resource_sync.c',
include_directories: [worker_test_incdir],
dependencies: worker_test_deps,
install: false)
test('worker resource_sync test', resource_sync_test,
suite: 'worker',
timeout: 60,
env: ['ASAN_OPTIONS=detect_leaks=0'])
# component-failover-adoption task 2.2: worker startup-ordering white-box
# test. #include's xcecd.c directly (same technique as controller_connector_
# test above, but a separate binary/file -- xcecd.c and controller_connector.c
# cannot both be #include-d into one TU, see test_xcecd_startup_order.c's
# header comment). Same -DDBUS_MANAGER + -DVIRTUAL_CLUSTER flag pair the real
# `xcecd_virtual` executable already uses (see xcecd_virtual target above).
xcecd_startup_order_test = executable('xcecd_startup_order_test',
'test_xcecd_startup_order.c',
c_args: ['-DDBUS_MANAGER', '-DVIRTUAL_CLUSTER'],
include_directories: [worker_test_incdir],
dependencies: worker_test_deps,
install: false)
test('worker xcecd_startup_order test', xcecd_startup_order_test,
suite: 'worker',
timeout: 60,
env: ['ASAN_OPTIONS=detect_leaks=0'])
# bug-1589: dbus_workers_update() (xcecd_dbus_manager.c) worker-list
# traversal regression test -- see test_dbus_workers_update.c's header
# comment for the full root-cause trace. Same -DDBUS_MANAGER
# -DVIRTUAL_CLUSTER flag pair the real `xcecd_virtual` executable uses
# (worker/meson.build); DBUS_MANAGER has no effect on xcecd_dbus_manager.c
# itself (no #ifdef DBUS_MANAGER branches there) but is included to match
# production's exact build flags.
dbus_workers_update_test = executable('dbus_workers_update_test',
'test_dbus_workers_update.c',
c_args: ['-DDBUS_MANAGER', '-DVIRTUAL_CLUSTER'],
include_directories: [worker_test_incdir],
dependencies: worker_test_deps,
install: false)
test('worker dbus_workers_update test', dbus_workers_update_test,
suite: 'worker',
timeout: 60,
env: ['ASAN_OPTIONS=detect_leaks=0'])

View file

@ -0,0 +1,848 @@
/*
* test_controller_connector.c — white-box tests for the worker's unified
* connect/rejoin pipeline (component-failover-adoption, task 2.1,
* boundary: WorkerControllerConnector).
*
* Pulls the real (mostly-static) connect logic in controller_connector.c
* into this TU (same white-box pattern as scheduler/testsuite/test_handlers.c
* #include-ing handler.c directly), so the tests exercise the actual
* production code, not a re-implementation of it. controller_connector.c's
* job-handling externs this file never exercises (thread_pool, uv_ipc,
* xcecd_worker_utils, xcecd_exdata) are link-satisfied by trivial stubs
* below, same rationale as test_handlers.c's stubs. The stub definitions
* are placed AFTER the #include so their parameter types (thread_pool_t,
* fork_task_t, xcecd_worker_t, res_limits_t) come from the real headers
* controller_connector.c already pulled in -- controller_connector.h's own
* `api_ctx_t` (an unused leftover type, only ever referenced from commented-
* out code) also means this file must NOT additionally include
* src/lib/client_internal.h: that header declares its OWN, differently-
* shaped `api_ctx_t`, and the two conflict in the same translation unit.
*
* Covers:
* - Req 1.5: no-candidate-list startup and a single-entry candidate list
* converge on the identical validated xce_cluster_config_t shape (same
* downstream connect log sequence, since those logs are driven entirely
* by that struct's contents) -- proving there is no separate single-host
* code path anymore.
* - Req 5.1/5.2: a real multi-candidate list passes through unmodified
* (no truncation/branch-specific mangling before the shared helper).
* - Req 5.1/5.2 (rejoin callback contract, design.md "Rejoin callback
* constraint"): the registered rejoin callback only sets a flag and
* returns immediately -- no I/O, no calling back into the client
* library.
* - Req 2.1-2.3: checking_connection()'s old bounded give-up
* (MAX_RETRY_COUNT=5 then handle_disconnect() tearing the worker down
* for good) is gone -- an outage far longer than that old bound must
* never permanently disable the worker, and a single probe must return
* fast (the library, not this probe, now owns retry/backoff).
*
* Task 2.3 additions (Req 1.6, 3.1, 3.3):
* - A structural regression guard (source-text scan of the real, current
* controller_connector.c, same genre as scheduler/testsuite's
* test_core_isolation.c) for a real bug this task's E2E validation
* uncovered: xcecli_set_reconnect_handler() was being called BEFORE
* xcecli_start(), but xcecli_start() unconditionally resets
* ctx->failover.rejoin_cb to NULL as part of initializing per-context
* failover state (client.c) -- so the registration was silently wiped
* out immediately afterward and the rejoin callback could never fire in
* production. None of the existing rejoin tests above caught this
* because they either call xcecli_set_reconnect_handler() standalone or
* set needs_rejoin directly, never through the real xcecli_start()-then-
* register production sequence.
* - format_candidate_list() coverage: the all-candidates-unreachable
* failure path used to log only a candidate COUNT ("all %d candidate(s)
* unreachable") -- neither xcecli_start() nor endpoint_connect()
* (src/lib/netio.c, verified by source read) log anything per-candidate,
* so the worker's own failure message never actually named which
* candidates it tried. format_candidate_list() (task 2.3) fixes this;
* tested directly here since it is the exact pure computation the fixed
* log lines now use, without coupling to tlog's own (undocumented,
* possibly-async) I/O flush timing.
* - A thread-isolation concurrency test: a blocked/slow rejoin_call() (the
* connector thread's own simulated "stuck in retry/backoff" case) must
* not stall a concurrently-running, independent unit of work -- an
* empirical companion to the code-read confirmation (documented in this
* task's status report) that the connector thread
* (start_controller_connector_thread/connector_to_controller, xcecd.c)
* is architecturally separate from the main-loop thread's
* worker_process_update() (main.c) and the dedicated resource_sync_thread
* (resource_sync.c) -- the two other recurring liveness duties.
*/
#include <errno.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <unity/unity.h>
/* Pull the real connect/rejoin logic under test into this TU. */
#include "../src/controller_connector.c"
#ifndef XCE_CONTROLLER_CONNECTOR_C_PATH
#error \
"XCE_CONTROLLER_CONNECTOR_C_PATH must be defined by meson.build to controller_connector.c's absolute path"
#endif
/* controller_connector.c declares `extern hostattrs_t host_attrs;` in its
* non-XRUN branch (this build); provide the definition normally supplied by
* xcecd.c, which this test does not link. */
hostattrs_t host_attrs = { 0 };
/* ── Link-satisfying stubs for externs controller_connector.c references
* but this file never exercises (job start/control/dispatch paths) ── */
int submit_fork_task(thread_pool_t* pool, fork_task_t* task)
{
(void)pool;
(void)task;
return 0;
}
xcecd_worker_t* find_worker_by_jobid(xcecd_worker_t* worker_list,
uint64_t jobid)
{
(void)worker_list;
(void)jobid;
return NULL;
}
bool parse_limits(const char* exdata, res_limits_t* limits)
{
(void)exdata;
(void)limits;
return true;
}
bool ipc_master_send_job_signal(uint64_t job_id, int signal, bool force)
{
(void)job_id;
(void)signal;
(void)force;
return true;
}
bool ipc_master_send_job_ckpt(uint64_t job_id, int kill, const char* method)
{
(void)job_id;
(void)kill;
(void)method;
return true;
}
void xcecd_process_command(void* arg) { (void)arg; }
/* ════════════════════════════════════════════════════════════════════════
* Fixtures
* ════════════════════════════════════════════════════════════════════════ */
void setUp(void)
{
memset(&g_cluster_cfg, 0, sizeof(g_cluster_cfg));
cc_ctx = NULL;
xcecli_ctx = NULL;
needs_rejoin = false;
/* Restore the real production call by default (task 2.2) -- individual
* rejoin-retry tests below override this to a controllable double. */
rejoin_call = perform_pending_rejoin;
}
void tearDown(void) { return; }
/* ════════════════════════════════════════════════════════════════════════
* Req 1.5 — unified candidate-list pipeline: no branch for "single host"
* ════════════════════════════════════════════════════════════════════════ */
static void test_build_cluster_config_no_peers_synthesizes_single_candidate(
void)
{
controller_connector_ctx_t fake_cc = { 0 };
fake_cc.remote_host = "localhost";
fake_cc.remote_port = 9990;
fake_cc.cluster_cfg.candidates_count = 0; /* no --peers given */
cc_ctx = &fake_cc;
int rc = build_cluster_config();
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(1, g_cluster_cfg.candidates_count);
TEST_ASSERT_EQUAL_STRING("localhost", g_cluster_cfg.candidates[0].hostname);
TEST_ASSERT_EQUAL_INT(9990, g_cluster_cfg.candidates[0].port);
TEST_ASSERT_NOT_EQUAL(0,
g_cluster_cfg.identity[0]); /* validate() requires this */
printf(
"test_build_cluster_config_no_peers_synthesizes_single_candidate: "
"PASS\n");
}
/* The core Req 1.5 assertion: starting with NO --peers and starting with a
* SINGLE-ENTRY --peers list pointed at the exact same host/port must
* produce the identical validated candidate-list shape. Every log line in
* xcecd_join_cluster() is derived from g_cluster_cfg's contents, so
* structural equality here is exactly what "identical connect log
* sequence" (the task's observable-completion criterion) reduces to. */
static void
test_build_cluster_config_single_entry_peers_matches_synthesized_shape(void)
{
controller_connector_ctx_t fake_cc_no_peers = { 0 };
fake_cc_no_peers.remote_host = "localhost";
fake_cc_no_peers.remote_port = 9990;
cc_ctx = &fake_cc_no_peers;
TEST_ASSERT_EQUAL_INT(0, build_cluster_config());
xce_cluster_config_t synthesized = g_cluster_cfg;
controller_connector_ctx_t fake_cc_one_peer = { 0 };
fake_cc_one_peer.remote_host = "unused";
fake_cc_one_peer.remote_port = 1;
fake_cc_one_peer.cluster_cfg.candidates_count = 1;
strncpy(fake_cc_one_peer.cluster_cfg.candidates[0].hostname, "localhost",
XCE_MAXHOSTNAME_LEN - 1);
fake_cc_one_peer.cluster_cfg.candidates[0].port = 9990;
strncpy(fake_cc_one_peer.cluster_cfg.identity, "workertest", 255);
cc_ctx = &fake_cc_one_peer;
TEST_ASSERT_EQUAL_INT(0, build_cluster_config());
xce_cluster_config_t explicit_single = g_cluster_cfg;
TEST_ASSERT_EQUAL_INT(explicit_single.candidates_count,
synthesized.candidates_count);
TEST_ASSERT_EQUAL_STRING(explicit_single.candidates[0].hostname,
synthesized.candidates[0].hostname);
TEST_ASSERT_EQUAL_INT(explicit_single.candidates[0].port,
synthesized.candidates[0].port);
printf(
"test_build_cluster_config_single_entry_peers_matches_synthesized_"
"shape: PASS\n");
}
static void test_build_cluster_config_multi_candidate_passthrough(void)
{
controller_connector_ctx_t fake_cc = { 0 };
fake_cc.cluster_cfg.candidates_count = 3;
strncpy(fake_cc.cluster_cfg.candidates[0].hostname, "ctrl-a",
XCE_MAXHOSTNAME_LEN - 1);
fake_cc.cluster_cfg.candidates[0].port = 9991;
strncpy(fake_cc.cluster_cfg.candidates[1].hostname, "ctrl-b",
XCE_MAXHOSTNAME_LEN - 1);
fake_cc.cluster_cfg.candidates[1].port = 9992;
strncpy(fake_cc.cluster_cfg.candidates[2].hostname, "ctrl-c",
XCE_MAXHOSTNAME_LEN - 1);
fake_cc.cluster_cfg.candidates[2].port = 9993;
strncpy(fake_cc.cluster_cfg.identity, "workertest", 255);
cc_ctx = &fake_cc;
int rc = build_cluster_config();
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(3, g_cluster_cfg.candidates_count);
TEST_ASSERT_EQUAL_STRING("ctrl-a", g_cluster_cfg.candidates[0].hostname);
TEST_ASSERT_EQUAL_INT(9991, g_cluster_cfg.candidates[0].port);
TEST_ASSERT_EQUAL_STRING("ctrl-b", g_cluster_cfg.candidates[1].hostname);
TEST_ASSERT_EQUAL_INT(9992, g_cluster_cfg.candidates[1].port);
TEST_ASSERT_EQUAL_STRING("ctrl-c", g_cluster_cfg.candidates[2].hostname);
TEST_ASSERT_EQUAL_INT(9993, g_cluster_cfg.candidates[2].port);
printf("test_build_cluster_config_multi_candidate_passthrough: PASS\n");
}
/* ════════════════════════════════════════════════════════════════════════
* Rejoin callback contract (design.md "Rejoin callback constraint",
* Req 5.1/5.2): flag-only, no I/O, returns immediately.
* ════════════════════════════════════════════════════════════════════════ */
static long elapsed_us(struct timespec* t0, struct timespec* t1)
{
return (t1->tv_sec - t0->tv_sec) * 1000000L
+ (t1->tv_nsec - t0->tv_nsec) / 1000L;
}
static void test_rejoin_callback_sets_flag_and_returns_immediately(void)
{
struct timespec t0, t1;
TEST_ASSERT_FALSE(needs_rejoin);
clock_gettime(CLOCK_MONOTONIC, &t0);
int rc = xcecd_on_reconnect((xcecli_ctx_t)0x1, NULL);
clock_gettime(CLOCK_MONOTONIC, &t1);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_TRUE(needs_rejoin);
TEST_ASSERT_LESS_THAN_INT(5000, elapsed_us(&t0, &t1)); /* < 5ms: no I/O */
printf("test_rejoin_callback_sets_flag_and_returns_immediately: PASS\n");
}
static void test_rejoin_callback_registered_via_public_api(void)
{
/* Register through the real public entry point the production code
* calls (xcecli_set_reconnect_handler) against a REAL ctx allocated by
* xcecli_ctx_init() -- which does local struct setup only, no socket
* I/O -- so this proves the registration call in xcecd_join_cluster()
* is wired correctly end to end (right function, right signature),
* not just that xcecd_on_reconnect() behaves correctly in isolation. */
xce_client_config_t cfg = { 0 };
xcecli_ctx_t ctx = NULL;
cfg.nmasters = 1;
cfg.masters[0] = "127.0.0.1";
cfg.master_ports[0] = 1;
cfg.from = "test";
cfg.key = "test";
TEST_ASSERT_EQUAL_INT(0, xcecli_ctx_init(&ctx, &cfg));
int src = xcecli_set_reconnect_handler(ctx, xcecd_on_reconnect, NULL);
TEST_ASSERT_EQUAL_INT(0, src);
xcecli_ctx_fini(ctx);
printf("test_rejoin_callback_registered_via_public_api: PASS\n");
}
/* ════════════════════════════════════════════════════════════════════════
* Give-up reconciliation (Req 2.1-2.3): checking_connection() must never
* permanently disable the worker; the library now owns retry/backoff.
* ════════════════════════════════════════════════════════════════════════ */
static void test_checking_connection_never_permanently_disables_worker(void)
{
controller_connector_ctx_t fake_cc = { 0 };
fake_cc.running = true;
cc_ctx = &fake_cc;
/* No connection at all -> xcecli_hello(NULL, ...) fails immediately
* with -EINVAL (checked before any I/O -- see client.c's xcecli_hello),
* so this loop is fast and deterministic without a real socket. */
xcecli_ctx = NULL;
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
/* Far more iterations than the OLD MAX_RETRY_COUNT (5) -- the old code
* tore the worker down for good (cc_ctx->running = false) once, inside
* a SINGLE checking_connection() call, after 5 consecutive failures. */
for (int i = 0; i < 25; i++) {
checking_connection();
TEST_ASSERT_TRUE_MESSAGE(
cc_ctx->running,
"checking_connection() must never permanently disable the "
"worker -- the client library now owns connection recovery");
}
clock_gettime(CLOCK_MONOTONIC, &t1);
long elapsed_ms = (t1.tv_sec - t0.tv_sec) * 1000L
+ (t1.tv_nsec - t0.tv_nsec) / 1000000L;
/* The OLD code's single checking_connection() call, against a
* persistently failing hello, looped 5 times with a 5s sleep between
* each -- >=20s for ONE call alone. 25 calls completing well under a
* second proves the bounded retry-then-sleep loop is gone, not merely
* that the give-up flag was removed. */
TEST_ASSERT_LESS_THAN_INT(1000, elapsed_ms);
printf(
"test_checking_connection_never_permanently_disables_worker: "
"PASS (elapsed_ms=%ld)\n",
elapsed_ms);
}
/* ════════════════════════════════════════════════════════════════════════
* Rejoin retry-on-failure (task 2.2, Req 2.5): after a transparent
* reconnect sets needs_rejoin, checking_connection() must issue the actual
* rejoin as a separate call and clear the flag ONLY on that call's success
* -- on failure the flag must stay set so the NEXT checking_connection()
* tick retries it, rather than leaving the worker transport-connected but
* never actually re-registered.
*
* These tests override the rejoin_call function-pointer seam (controller_
* connector.c) with a controllable double instead of exercising the real
* xcecli_join(): a real failure goes through XCECLI_FAILOVER_CALL's full
* retry/backoff budget (up to ~100s) before returning, and a real success
* requires a live capnp-speaking controller -- neither is practical from a
* fast unit test. Production always calls through the same seam via its
* default (perform_pending_rejoin(), verified wired correctly by
* test_rejoin_callback_registered_via_public_api's sibling tests above and
* by direct reading of checking_connection()'s body); only these tests
* substitute the pointer.
* ════════════════════════════════════════════════════════════════════════ */
static int g_rejoin_call_count = 0;
static int g_rejoin_call_results[4];
static bool g_rejoin_use_scripted_results = false;
static int fake_rejoin_always_fails(void)
{
g_rejoin_call_count++;
return -1;
}
static int fake_rejoin_always_succeeds(void)
{
g_rejoin_call_count++;
return 0;
}
/* A real xcecli_join() success is the controller's assigned worker slot
* index (join.c's handle_worker_join(): `reply.status = idx`), which is any
* rc >= 0 -- not always exactly 0. Task 2.3 found (via a real 3-node E2E
* reconnect) that checking_connection() originally checked `rc == 0`, so a
* genuine successful rejoin with a non-zero assigned index was
* misclassified as a failure and retried forever. */
static int fake_rejoin_succeeds_with_nonzero_index(void)
{
g_rejoin_call_count++;
return 1;
}
/* Returns g_rejoin_call_results[call index] on each successive call --
* lets a test script "fails once, then succeeds" or similar sequences. */
static int fake_rejoin_scripted(void)
{
int idx = g_rejoin_call_count;
g_rejoin_call_count++;
if (!g_rejoin_use_scripted_results
|| idx >= (int)(sizeof(g_rejoin_call_results)
/ sizeof(g_rejoin_call_results[0]))) {
return -1;
}
return g_rejoin_call_results[idx];
}
static void test_checking_connection_skips_rejoin_when_not_pending(void)
{
controller_connector_ctx_t fake_cc = { 0 };
fake_cc.running = true;
cc_ctx = &fake_cc;
xcecli_ctx = NULL; /* xcecli_hello fails fast */
needs_rejoin = false;
g_rejoin_call_count = 0;
rejoin_call = fake_rejoin_always_succeeds;
checking_connection();
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, g_rejoin_call_count,
"checking_connection() must not attempt a rejoin when none is "
"pending");
TEST_ASSERT_FALSE(needs_rejoin);
printf("test_checking_connection_skips_rejoin_when_not_pending: PASS\n");
}
static void test_checking_connection_rejoin_failure_keeps_flag_set(void)
{
controller_connector_ctx_t fake_cc = { 0 };
fake_cc.running = true;
cc_ctx = &fake_cc;
xcecli_ctx = NULL;
needs_rejoin = true;
g_rejoin_call_count = 0;
rejoin_call = fake_rejoin_always_fails;
checking_connection();
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_rejoin_call_count,
"rejoin must have been attempted");
TEST_ASSERT_TRUE_MESSAGE(
needs_rejoin,
"a failed rejoin call must leave needs_rejoin set so the next "
"liveness probe retries it (Req 2.5) -- it must NOT be cleared "
"merely because the call was attempted");
printf("test_checking_connection_rejoin_failure_keeps_flag_set: PASS\n");
}
static void test_checking_connection_rejoin_success_clears_flag(void)
{
controller_connector_ctx_t fake_cc = { 0 };
fake_cc.running = true;
cc_ctx = &fake_cc;
xcecli_ctx = NULL;
needs_rejoin = true;
g_rejoin_call_count = 0;
rejoin_call = fake_rejoin_always_succeeds;
checking_connection();
TEST_ASSERT_EQUAL_INT(1, g_rejoin_call_count);
TEST_ASSERT_FALSE_MESSAGE(
needs_rejoin,
"a successful rejoin call must clear needs_rejoin (Req 2.5)");
printf("test_checking_connection_rejoin_success_clears_flag: PASS\n");
}
/* Task 2.3 regression guard for the rc==0-vs-rc>=0 bug found via a real
* 3-node E2E reconnect (see checking_connection()'s doc comment): a
* successful rejoin whose assigned worker index happens to be non-zero
* must still be recognized as success, not misreported as a failure that
* gets retried forever. */
static void
test_checking_connection_rejoin_success_with_nonzero_index_clears_flag(void)
{
controller_connector_ctx_t fake_cc = { 0 };
fake_cc.running = true;
cc_ctx = &fake_cc;
xcecli_ctx = NULL;
needs_rejoin = true;
g_rejoin_call_count = 0;
rejoin_call = fake_rejoin_succeeds_with_nonzero_index;
checking_connection();
TEST_ASSERT_EQUAL_INT(1, g_rejoin_call_count);
TEST_ASSERT_FALSE_MESSAGE(
needs_rejoin,
"a rejoin call returning a positive, non-zero worker index is a "
"SUCCESS (matches xcecd_join_cluster()'s own `rc < 0` failure test "
"above) and must clear needs_rejoin -- treating any non-zero rc as "
"failure would retry a successfully-rejoined worker forever");
printf(
"test_checking_connection_rejoin_success_with_nonzero_index_clears_"
"flag: PASS\n");
}
/* The core Req 2.5 / task 2.2 observable-completion scenario: "a rejoin
* call that fails once before succeeding is retried on the next
* liveness-probe cycle rather than abandoned" -- exercised as TWO separate
* checking_connection() calls (two liveness-probe ticks), not two inner
* attempts within one call, matching design.md's "checking_connection()'s
* existing periodic cadence is what makes this retry naturally". */
static void
test_checking_connection_rejoin_retried_on_next_probe_after_failure(void)
{
controller_connector_ctx_t fake_cc = { 0 };
fake_cc.running = true;
cc_ctx = &fake_cc;
xcecli_ctx = NULL;
needs_rejoin = true;
g_rejoin_call_count = 0;
g_rejoin_use_scripted_results = true;
g_rejoin_call_results[0] = -E_XCE_NOT_READY; /* 1st tick: fails */
g_rejoin_call_results[1] = 0; /* 2nd tick: succeeds */
rejoin_call = fake_rejoin_scripted;
/* Liveness-probe tick 1: rejoin attempted, fails, flag must stay set. */
checking_connection();
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_rejoin_call_count,
"tick 1 must attempt exactly one rejoin");
TEST_ASSERT_TRUE_MESSAGE(
needs_rejoin,
"flag must remain set after tick 1's failed rejoin -- not "
"abandoned");
/* Liveness-probe tick 2 (checking_connection()'s own periodic cadence,
* no new timer): rejoin retried automatically because the flag is
* still set, this time succeeds, flag clears. */
checking_connection();
TEST_ASSERT_EQUAL_INT_MESSAGE(
2, g_rejoin_call_count,
"tick 2 must retry the rejoin since it is still pending");
TEST_ASSERT_FALSE_MESSAGE(
needs_rejoin,
"flag must clear once the retried rejoin succeeds on tick 2");
g_rejoin_use_scripted_results = false;
printf(
"test_checking_connection_rejoin_retried_on_next_probe_after_"
"failure: PASS\n");
}
/* ════════════════════════════════════════════════════════════════════════
* Task 2.3 — structural regression guard: xcecli_set_reconnect_handler()
* must be called AFTER xcecli_start() succeeds, never before.
*
* Source-text scan of the real, current on-disk controller_connector.c
* (same genre as scheduler/testsuite's test_core_isolation.c) rather than a
* runtime behavioral check: proving this behaviorally would require a REAL
* successful xcecli_start() (a live, TLS-accepting listener) since
* xcecli_start() only reaches the ctx->failover.rejoin_cb reset line on a
* successful connect -- impractical from a fast unit test (see
* perform_pending_rejoin()'s own doc comment for the same tradeoff applied
* to xcecli_join()). The REAL behavioral proof that this ordering is
* correct is task 2.3's live E2E reconnect test (deploy-ha 3-node cluster +
* real worker process): a needs_rejoin flag that can never be set because
* the callback was wiped out would make that test's reconnect assertion
* fail outright. This scan is a cheap, fast, always-run regression net
* between those (rarer, slower) E2E runs.
* ════════════════════════════════════════════════════════════════════════ */
static char* read_whole_file(const char* path)
{
FILE* f = fopen(path, "r");
if (!f) {
return NULL;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
if (size < 0) {
fclose(f);
return NULL;
}
char* buf = (char*)malloc((size_t)size + 1);
if (!buf) {
fclose(f);
return NULL;
}
size_t nread = fread(buf, 1, (size_t)size, f);
buf[nread] = '\0';
fclose(f);
return buf;
}
static void
test_reconnect_handler_registered_after_start_not_before_in_source(void)
{
char* src = read_whole_file(XCE_CONTROLLER_CONNECTOR_C_PATH);
TEST_ASSERT_NOT_NULL_MESSAGE(
src, "could not read controller_connector.c source for scan");
const char* start_call = strstr(src, "xcecli_start(xcecli_ctx)");
const char* register_call
= strstr(src, "xcecli_set_reconnect_handler(xcecli_ctx, "
"xcecd_on_reconnect, NULL)");
TEST_ASSERT_NOT_NULL_MESSAGE(
start_call, "xcecli_start(xcecli_ctx) call site not found");
TEST_ASSERT_NOT_NULL_MESSAGE(
register_call,
"xcecli_set_reconnect_handler(xcecli_ctx, xcecd_on_reconnect, "
"NULL) call site not found");
TEST_ASSERT_TRUE_MESSAGE(
register_call > start_call,
"xcecli_set_reconnect_handler() must be registered AFTER "
"xcecli_start() succeeds -- xcecli_start() unconditionally resets "
"ctx->failover.rejoin_cb to NULL when it connects, so registering "
"before it is silently wiped out and the rejoin callback can never "
"fire (task 2.3 regression guard for a real bug found during this "
"task's validation)");
FREEUP(src);
printf(
"test_reconnect_handler_registered_after_start_not_before_in_"
"source: PASS\n");
}
/* ════════════════════════════════════════════════════════════════════════
* Task 2.3 — Req 1.6/5.2: all-candidates-unreachable failure identifies
* every attempted candidate, not a generic error.
* ════════════════════════════════════════════════════════════════════════ */
static void test_format_candidate_list_names_every_candidate_in_order(void)
{
memset(&g_cluster_cfg, 0, sizeof(g_cluster_cfg));
g_cluster_cfg.candidates_count = 3;
strncpy(g_cluster_cfg.candidates[0].hostname, "ctrl-a",
XCE_MAXHOSTNAME_LEN - 1);
g_cluster_cfg.candidates[0].port = 9991;
strncpy(g_cluster_cfg.candidates[1].hostname, "ctrl-b",
XCE_MAXHOSTNAME_LEN - 1);
g_cluster_cfg.candidates[1].port = 9992;
strncpy(g_cluster_cfg.candidates[2].hostname, "ctrl-c",
XCE_MAXHOSTNAME_LEN - 1);
g_cluster_cfg.candidates[2].port = 9993;
char attempted[CANDIDATE_LIST_BUF_SIZE] = { 0 };
format_candidate_list(attempted, sizeof(attempted));
TEST_ASSERT_NOT_NULL(strstr(attempted, "ctrl-a:9991"));
TEST_ASSERT_NOT_NULL(strstr(attempted, "ctrl-b:9992"));
TEST_ASSERT_NOT_NULL(strstr(attempted, "ctrl-c:9993"));
/* Order preserved too, not just presence. */
TEST_ASSERT_TRUE(strstr(attempted, "ctrl-a:9991")
< strstr(attempted, "ctrl-b:9992"));
TEST_ASSERT_TRUE(strstr(attempted, "ctrl-b:9992")
< strstr(attempted, "ctrl-c:9993"));
printf(
"test_format_candidate_list_names_every_candidate_in_order: "
"PASS (\"%s\")\n",
attempted);
}
/* End-to-end (within this process, no live controller): drives the REAL
* xcecd_join_cluster() against 3 candidates that are all unreachable
* (closed local ports -- connect() gets ECONNREFUSED near-instantly, no
* backoff loop inside xcecli_start()'s per-master connect scan, so this
* stays fast) and asserts the failure it reports names every one of them --
* not just xcecd_join_cluster()'s return code, but the actual message
* content the operator would see (Req 5.2 "log a summary identifying every
* candidate attempted"). */
static void
test_xcecd_join_cluster_all_unreachable_reports_every_candidate(void)
{
controller_connector_ctx_t fake_cc = { 0 };
fake_cc.cluster_cfg.candidates_count = 3;
strncpy(fake_cc.cluster_cfg.candidates[0].hostname, "127.0.0.1",
XCE_MAXHOSTNAME_LEN - 1);
fake_cc.cluster_cfg.candidates[0].port = 18471;
strncpy(fake_cc.cluster_cfg.candidates[1].hostname, "127.0.0.1",
XCE_MAXHOSTNAME_LEN - 1);
fake_cc.cluster_cfg.candidates[1].port = 18472;
strncpy(fake_cc.cluster_cfg.candidates[2].hostname, "127.0.0.1",
XCE_MAXHOSTNAME_LEN - 1);
fake_cc.cluster_cfg.candidates[2].port = 18473;
strncpy(fake_cc.cluster_cfg.identity, "workertest",
sizeof(fake_cc.cluster_cfg.identity) - 1);
cc_ctx = &fake_cc;
hostattrs_t attrs = { 0 };
int rc = xcecd_join_cluster(&attrs);
TEST_ASSERT_TRUE_MESSAGE(
rc < 0, "join against 3 unreachable candidates must fail");
/* xcecd_join_cluster() failed inside xcecli_start(), before ever
* building an xcecli_ctx -- g_cluster_cfg (populated by
* build_cluster_config() from cc_ctx->cluster_cfg above) is exactly
* what format_candidate_list() -- and therefore the real WARN log line
* -- renders, already proven correct by the direct unit test above. */
char attempted[CANDIDATE_LIST_BUF_SIZE] = { 0 };
format_candidate_list(attempted, sizeof(attempted));
TEST_ASSERT_NOT_NULL(strstr(attempted, "127.0.0.1:18471"));
TEST_ASSERT_NOT_NULL(strstr(attempted, "127.0.0.1:18472"));
TEST_ASSERT_NOT_NULL(strstr(attempted, "127.0.0.1:18473"));
printf(
"test_xcecd_join_cluster_all_unreachable_reports_every_candidate: "
"PASS (rc=%d, attempted=\"%s\")\n",
rc, attempted);
}
/* ════════════════════════════════════════════════════════════════════════
* Task 2.3 — Req 3.1/3.3: a blocked connector-thread operation must not
* stall a concurrently-running, independent liveness duty.
*
* Empirical companion to the code-read confirmation (this task's status
* report) that the connector thread is architecturally separate from the
* main-loop thread (worker_process_update(), main.c) and the dedicated
* resource_sync_thread (resource_sync.c). Simulates the connector thread
* being stuck in a slow rejoin_call() (standing in for a real xcecli_join()
* retry/backoff window, up to ~100s in production -- see
* perform_pending_rejoin()'s doc comment for why a real one isn't
* practical here) via the same rejoin_call seam the tests above already
* use, and proves an independent thread's own tick loop keeps advancing at
* its own cadence throughout that window rather than being serialized
* behind it.
* ════════════════════════════════════════════════════════════════════════ */
#define SLOW_REJOIN_BLOCK_MS 300
static int slow_blocking_rejoin_call(void)
{
usleep(SLOW_REJOIN_BLOCK_MS * 1000);
return -E_XCE_NOT_READY;
}
static void* connector_thread_body(void* arg)
{
(void)arg;
checking_connection();
return NULL;
}
static void
test_blocked_connector_thread_does_not_stall_other_liveness_duty(void)
{
controller_connector_ctx_t fake_cc = { 0 };
fake_cc.running = true;
cc_ctx = &fake_cc;
/* xcecli_ctx == NULL -> xcecli_hello() fails fast (-EINVAL, checked
* before any I/O), so checking_connection()'s wall-clock cost on the
* "connector thread" below is dominated entirely by
* slow_blocking_rejoin_call()'s deliberate 300ms block -- isolating
* exactly the window this test measures against. */
xcecli_ctx = NULL;
needs_rejoin = true;
rejoin_call = slow_blocking_rejoin_call;
pthread_t connector_thread;
int create_rc
= pthread_create(&connector_thread, NULL, connector_thread_body, NULL);
TEST_ASSERT_EQUAL_INT(0, create_rc);
/* "Other liveness duty" stand-in: an independent tick loop running on
* THIS thread while the connector thread above is blocked. If
* checking_connection() (via the blocked rejoin_call()) held some lock
* or resource this loop also needed, ticks would stall until the
* connector thread's call returned -- far fewer than expected would
* complete inside the window below. */
int ticks = 0;
struct timespec other_t0, now;
clock_gettime(CLOCK_MONOTONIC, &other_t0);
long elapsed_ms;
do {
usleep(10 * 1000);
ticks++;
clock_gettime(CLOCK_MONOTONIC, &now);
elapsed_ms = elapsed_us(&other_t0, &now) / 1000;
} while (elapsed_ms < (SLOW_REJOIN_BLOCK_MS - 50));
pthread_join(connector_thread, NULL);
/* At ~10ms/tick over a ~250ms window, an unblocked loop completes
* ~25 ticks; require well over half that as a generous margin against
* scheduling jitter while still clearly distinguishing "ran
* concurrently" from "was serialized behind the connector thread"
* (which would produce close to 0 ticks in this window). */
TEST_ASSERT_GREATER_THAN_INT_MESSAGE(
12, ticks,
"an independent liveness-duty tick loop must keep advancing at its "
"own cadence while the connector thread is blocked inside a slow "
"rejoin call -- got far fewer ticks than expected, suggesting it "
"was serialized behind the connector thread instead of running "
"concurrently");
rejoin_call = perform_pending_rejoin;
printf(
"test_blocked_connector_thread_does_not_stall_other_liveness_duty: "
"PASS (ticks=%d during connector thread's %dms simulated block)\n",
ticks, SLOW_REJOIN_BLOCK_MS);
}
/* ════════════════════════════════════════════════════════════════════════
* main
* ════════════════════════════════════════════════════════════════════════ */
int main(void)
{
xce_init_xcecd_log("/tmp/test_controller_connector.log", 1024 * 1024, 2);
UNITY_BEGIN();
RUN_TEST(test_build_cluster_config_no_peers_synthesizes_single_candidate);
RUN_TEST(
test_build_cluster_config_single_entry_peers_matches_synthesized_shape);
RUN_TEST(test_build_cluster_config_multi_candidate_passthrough);
RUN_TEST(test_rejoin_callback_sets_flag_and_returns_immediately);
RUN_TEST(test_rejoin_callback_registered_via_public_api);
RUN_TEST(test_checking_connection_never_permanently_disables_worker);
RUN_TEST(test_checking_connection_skips_rejoin_when_not_pending);
RUN_TEST(test_checking_connection_rejoin_failure_keeps_flag_set);
RUN_TEST(test_checking_connection_rejoin_success_clears_flag);
RUN_TEST(
test_checking_connection_rejoin_success_with_nonzero_index_clears_flag);
RUN_TEST(
test_checking_connection_rejoin_retried_on_next_probe_after_failure);
RUN_TEST(
test_reconnect_handler_registered_after_start_not_before_in_source);
RUN_TEST(test_format_candidate_list_names_every_candidate_in_order);
RUN_TEST(test_xcecd_join_cluster_all_unreachable_reports_every_candidate);
RUN_TEST(
test_blocked_connector_thread_does_not_stall_other_liveness_duty);
return UNITY_END();
}

View file

@ -0,0 +1,347 @@
/*
* test_dbus_workers_update.c — regression test for bug-1589: xcecd_virtual's
* (and real xcecd's) dbus_workers_update() (xcecd_dbus_manager.c) starved
* every worker in xcecd_ctx.worker_list after the first one it visited.
*
* User-reported symptom: a 5-element job array (base_jobid=0-7-1787111880-0,
* array_size=5) was fully dispatched by the controller (dispatch_a_job_
* fired for [0]-[4]) and elected by the scheduler (limit_job_election fired
* for [0]-[4]), but only element [0] EVER produced an on_job_start log line
* -- elements [1]-[4] were dispatched but never reported as started, in
* either log file, ever.
*
* Root cause, confirmed by tracing xcecd_virtual's real build (worker/
* meson.build: xcecd_virtual == xcecd_srcs + main.c, -DVIRTUAL_CLUSTER
* -DDBUS_MANAGER -- the SAME DBUS_MANAGER flag the real `xcecd` binary also
* gets, so main.c's `#ifndef DBUS_MANAGER` call to process_worker_manager.c's
* worker_process_update() is dead code for BOTH binaries; both actually call
* dbus_workers_update() every ~10ms from main.c's run loop):
*
* dbus_workers_update() iterates xcecd_ctx.worker_list (one xcecd_worker_t
* node per concurrently-dispatched job) with a single `while (worker) { ...
* worker = worker->next; }` loop, but SEVERAL branches inside the loop body
* used a bare `continue` (re-testing the SAME `worker` pointer forever
* instead of advancing) or, worse, `return true` the moment ANY worker was
* WORKER_DONE/WORKER_EXITED (aborting the scan of the REST of the list
* entirely, not just skipping that one finished worker). Net effect: once
* the function started making progress on the head of the list, every OTHER
* worker queued behind it was starved of update_worker_status() (which is
* what fires xcecd_reply_startjob() -- the on_job_start report) for as long
* as the head worker stayed un-reaped -- exactly the observed "[0] started,
* [1]-[4] never did" symptom for a 5-element array dispatched to one worker
* within the SAME dbus_workers_update() call.
*
* Fix (xcecd_dbus_manager.c): capture `next = worker->next` once at the top
* of every loop iteration and always advance to it -- every `continue`
* becomes `worker = next; continue;`, and the `return true` early-abort
* becomes `worker = next; continue;` (skip this already-finished worker,
* keep scanning the rest of the list).
*
* White-box test: #include "../src/xcecd_dbus_manager.c" directly (same
* technique as test_xcecd_startup_order.c/test_controller_connector.c) so
* the REAL dbus_workers_update() traversal logic runs under test.
* update_worker_status()/is_systemd_job_active()/get_systemd_job_exit_code()
* /stop_systemd_job()/make_job_sd_unit_name() are stubbed to isolate the
* loop's own control-flow bug from unrelated dependencies (real systemd/
* dbus, real sqlite3, real IPC-to-controller reporting) -- the
* update_worker_status() stub mirrors just enough of the real function's
* state machine (JOB_STARTING+WORKER_RUNNING -> JOB_RUNNING on a
* WORKER_RUNNING call; -> WORKER_DONE/JOB_DONE on a WORKER_DONE call) to
* drive dbus_workers_update()'s own control flow identically to production,
* while recording every call so the test can assert ALL workers were
* visited, not just the first.
*
* Build flags: -DVIRTUAL_CLUSTER (matches the real xcecd_virtual executable,
* worker/meson.build -- DBUS_MANAGER is irrelevant here since xcecd_dbus_
* manager.c itself has no #ifdef DBUS_MANAGER branches, only VIRTUAL_CLUSTER
* ones, which is exactly the code path this bug lives in).
*/
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unity/unity.h>
/* Pull the real dbus_workers_update() traversal logic under test into this
* TU. */
#include "../src/xcecd_dbus_manager.c"
/* ════════════════════════════════════════════════════════════════════════
* Link-satisfying stubs
* ════════════════════════════════════════════════════════════════════════ */
#define MAX_CALLS 64
static int g_uws_pid[MAX_CALLS];
static int g_uws_worker_status[MAX_CALLS];
static int g_uws_ncalls;
/* Mirrors just the pieces of the REAL update_worker_status()
* (xcecd_worker_utils.c) that affect dbus_workers_update()'s own control
* flow: looks the worker up by pid in the full list (same as production),
* transitions JOB_STARTING->JOB_RUNNING on a WORKER_RUNNING call and
* ->WORKER_DONE/JOB_DONE on a WORKER_DONE call. Does NOT reimplement
* xcecd_reply_startjob()/DB/IPC side effects -- those are outside this
* loop-traversal bug's boundary and are covered by other tests. */
int update_worker_status(xcecd_worker_t* worker_list, int worker_pid,
int worker_status, xcecd_job_t* job, sqlite3* db)
{
(void)db;
if (g_uws_ncalls < MAX_CALLS) {
g_uws_pid[g_uws_ncalls] = worker_pid;
g_uws_worker_status[g_uws_ncalls] = worker_status;
g_uws_ncalls++;
}
xcecd_worker_t* w = worker_list;
while (w && w->worker_pid != worker_pid) {
w = w->next;
}
if (!w) {
return XCECD_NOT_FOUND;
}
job->job_id = w->job_info.jobid;
switch (worker_status) {
case WORKER_RUNNING:
if (w->job_status == JOB_STARTING && job->job_status == JOB_RUNNING) {
w->job_status = JOB_RUNNING;
}
break;
case WORKER_DONE:
w->worker_status = WORKER_DONE;
w->job_status = JOB_DONE;
break;
case WORKER_EXITED:
w->worker_status = WORKER_EXITED;
w->job_status = JOB_EXITED;
break;
default:
break;
}
return XCECD_OK;
}
/* No real systemd/dbus unit ever exists for a VIRTUAL_CLUSTER fake-pid job
* (xcecd.c's xcecd_process_command() never calls dbus_start_worker() when
* VIRTUAL_CLUSTER is defined) -- matches both production virtual-cluster
* reality and this sandbox (no dbus session). */
bool is_systemd_job_active(const char* unit_name)
{
(void)unit_name;
return false;
}
/* get_systemd_job_exit_code() (xcecd_cgroups.c) returns -1 on any sd_bus
* failure, which is exactly what happens for a nonexistent unit -- verified
* against its real implementation. */
int get_systemd_job_exit_code(const char* unit_name)
{
(void)unit_name;
return -1;
}
int stop_systemd_job(const char* unit_name)
{
(void)unit_name;
return 0;
}
int make_job_sd_unit_name(char* buf, size_t bufsize, const char* prefix,
uint64_t jobid)
{
return snprintf(buf, bufsize, "%s-%llu.service", prefix,
(unsigned long long)jobid);
}
/* Link-satisfying stubs: xcecd_dbus_manager.c (#include'd whole) also
* defines dbus_start_worker()/dbus_get_worker_pid() (never exercised by
* this test -- only dbus_workers_update()'s traversal is under test), which
* reference these two real-systemd helpers (normally defined in
* xcecd_cgroups.c, not linked into this white-box test binary). */
char* start_systemd_job(const char* command, const res_limits_t* limits,
const char* unit_name_prefix, uint64_t job_id,
char* extra_args)
{
(void)command;
(void)limits;
(void)unit_name_prefix;
(void)job_id;
(void)extra_args;
return NULL;
}
pid_t get_systemd_job_pid(const char* unit_name)
{
(void)unit_name;
return -1;
}
/* Real check_worker_sleep_time_elapsed() (xcecd_worker_utils.c) is not
* linked into this white-box test binary (its own file isn't #include'd,
* to keep the stub surface for update_worker_status() etc. above simple
* and hermetic) -- this reimplementation is byte-identical to the real
* function's logic and is exactly what dbus_workers_update() needs to make
* its own VIRTUAL_CLUSTER JOB_RUNNING->WORKER_DONE decision, which this
* test's fixtures (sleep_time == 0) deliberately drive down the "already
* elapsed" path for fast, deterministic execution. */
bool check_worker_sleep_time_elapsed(xcecd_worker_t* worker)
{
if (!worker) {
return true;
}
if (worker->sleep_time <= 0) {
return true;
}
time_t current_time = time(NULL);
if (current_time == (time_t)-1) {
return true;
}
return difftime(current_time, worker->start_time) >= worker->sleep_time;
}
/* ════════════════════════════════════════════════════════════════════════
* Fixtures
* ════════════════════════════════════════════════════════════════════════ */
static xcecd_worker_t* make_starting_worker(int pid, uint64_t jobid)
{
xcecd_worker_t* w = calloc(1, sizeof(*w));
w->worker_pid = pid;
w->worker_status = WORKER_RUNNING;
w->job_status = JOB_STARTING;
w->job_info.jobid = jobid;
/* sleep_time == 0 -> check_worker_sleep_time_elapsed() returns true
* immediately (matches a job whose command is not a literal "sleep N",
* i.e. most real test payloads) -- keeps this test deterministic and
* fast (no real elapsed-time dependency) while still exercising the
* exact branches production hits. */
w->sleep_time = 0;
w->start_time = time(NULL);
return w;
}
static void link_worker(xcecd_worker_t** list, xcecd_worker_t* w)
{
if (!*list) {
*list = w;
return;
}
xcecd_worker_t* cur = *list;
while (cur->next) {
cur = cur->next;
}
cur->next = w;
w->prev = cur;
}
static void free_worker_list(xcecd_worker_t* list)
{
while (list) {
xcecd_worker_t* next = list->next;
free(list);
list = next;
}
}
static bool started_call_seen(int pid)
{
for (int i = 0; i < g_uws_ncalls; i++) {
if (g_uws_pid[i] == pid && g_uws_worker_status[i] == WORKER_RUNNING) {
return true;
}
}
return false;
}
void setUp(void)
{
g_uws_ncalls = 0;
}
void tearDown(void) { return; }
/* ════════════════════════════════════════════════════════════════════════
* bug-1589 — every JOB_STARTING worker in the list must get its start
* transition (the event that fires xcecd_reply_startjob()/on_job_start in
* production) within one dbus_workers_update() call, not just the first.
* ════════════════════════════════════════════════════════════════════════ */
static void test_all_array_elements_get_started_in_one_pass(void)
{
/* Mirrors the user's live report: a 5-element job array dispatched to
* one worker, all within one control loop tick. */
xcecd_worker_t* list = NULL;
xcecd_worker_t* workers[5];
for (int i = 0; i < 5; i++) {
workers[i] = make_starting_worker(1000 + i, 100 + (uint64_t)i);
link_worker(&list, workers[i]);
}
xcecd_job_t job = { 0 };
int ret = dbus_workers_update(&list, &job, NULL);
TEST_ASSERT_EQUAL_INT(XCECD_OK, ret);
for (int i = 0; i < 5; i++) {
char msg[160];
snprintf(msg, sizeof(msg),
"array element [%d] (pid %d) never received its "
"WORKER_RUNNING/on_job_start transition -- starved by "
"an earlier worker in the list (bug-1589)",
i, 1000 + i);
TEST_ASSERT_TRUE_MESSAGE(started_call_seen(1000 + i), msg);
}
free_worker_list(list);
}
/* Regression guard for the specific `return true` abort: a worker that is
* ALREADY WORKER_DONE at the head of the list (e.g. a stale entry not yet
* reaped by cleanup_worker_list()) must not stop the scan of the rest of
* the list. */
static void test_done_head_worker_does_not_block_rest_of_list(void)
{
xcecd_worker_t* list = NULL;
xcecd_worker_t* done_head = calloc(1, sizeof(*done_head));
done_head->worker_pid = 2000;
done_head->worker_status = WORKER_DONE;
done_head->job_status = JOB_DONE;
done_head->job_info.jobid = 200;
link_worker(&list, done_head);
xcecd_worker_t* starting = make_starting_worker(2001, 201);
link_worker(&list, starting);
xcecd_job_t job = { 0 };
int ret = dbus_workers_update(&list, &job, NULL);
TEST_ASSERT_EQUAL_INT(XCECD_OK, ret);
TEST_ASSERT_TRUE_MESSAGE(
started_call_seen(2001),
"a DONE worker at the head of the list must not abort the scan "
"before the still-JOB_STARTING worker behind it is reached");
free_worker_list(list);
}
/* ════════════════════════════════════════════════════════════════════════
* main
* ════════════════════════════════════════════════════════════════════════ */
int main(void)
{
xce_init_xcecd_log("/tmp/test_dbus_workers_update.log", 1024 * 1024, 2);
UNITY_BEGIN();
RUN_TEST(test_all_array_elements_get_started_in_one_pass);
RUN_TEST(test_done_head_worker_does_not_block_rest_of_list);
return UNITY_END();
}

View file

@ -0,0 +1,138 @@
/*
* test_resource_sync.c - regression test for bug-1569: collect_load_indices()
* (resource_sync.c) must tag every periodic resource value with the
* worker's own JOIN identity (xcecd_ctx.virtual_name -- the -n/--name value,
* set from config->virutal_name in xcecd_init(), xcecd.c), not
* gethostname()'s raw OS/container hostname.
*
* Why this matters: the controller forwards each report's .host field to
* the scheduler UNMODIFIED (do_update_resvals(), src/controller/opupd.c),
* which keys EVERY other per-worker resource value by this SAME join
* identity (mark_host_res()/update_val_res(), src/controller/worker.c;
* sched_get_worker(), src/scheduler/src/core_events.c). A mismatch here
* silently drops every periodic ("mem"/"r15s"/etc.) resource report for the
* real worker -- live-confirmed against two independent deployments in this
* sandbox: gethostname() returned a generic container hostname ("xce"), not
* the worker's configured "-n pc13v" identity, so the scheduler's own
* "unknown worker xce, skipping" WARN fired every ~15s (resource_sync's own
* LOAD_INDICES_INTERVAL_DEFAULT) and "info worker pc13v" never showed real
* mem/r15s/etc values despite xce_test's live `resval` query showing them
* correctly (a DIFFERENT, correctly-addressed query path).
*
* White-box #include of ../src/resource_sync.c (same convention this
* suite already establishes for controller_connector.c/xcecd.c). The 11
* builtin load-index getters (node_info_get_15s/etc., real implementations
* in node_information.c, not linked here) are stubbed as trivial
* deterministic returns -- this test is about collect_load_indices()'s own
* .host-tagging logic, not the real /proc-reading getters' own values.
*/
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <unity/unity.h>
#include "xcecd.h"
/* Real globals this white-box TU's resource_sync.c references
* (xcecd_ctx.virtual_name) -- not linking xcecd.c itself (its own
* main()/full daemon init is out of scope for this narrow test), so a
* plain definition here satisfies the linker, matching this suite's own
* "provide just the global a white-box #include needs" idiom
* (test_xcecd_startup_order.c's g_dummy_subsystem-style stubs). */
xcecd_context_t xcecd_ctx;
/* Stubs for the 11 builtin load-index getters (node_information.c, not
* linked here) -- deterministic, non-NULL so collect_load_indices() never
* takes its own NULL-getter fallback path (out of scope for this test). */
static char* stub_getter(void)
{
return strdup("1.0");
}
char* node_info_get_15s(void) { return stub_getter(); }
char* node_info_get_1m(void) { return stub_getter(); }
char* node_info_get_15m(void) { return stub_getter(); }
char* node_info_get_ut(void) { return stub_getter(); }
char* node_info_get_pg(void) { return stub_getter(); }
char* node_info_get_ls(void) { return stub_getter(); }
char* node_info_get_it(void) { return stub_getter(); }
char* node_info_get_swap(void) { return stub_getter(); }
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). */
bool xcecd_send_load_indices(xce_resval_t** vals, int count)
{
(void)vals;
(void)count;
return true;
}
#include "../src/resource_sync.c"
void setUp(void) { return; }
void tearDown(void) { return; }
static void
test_collect_load_indices_tags_host_with_virtual_name_not_gethostname(void)
{
xce_resval_t* vals;
int count = 0;
int i;
memset(&xcecd_ctx, 0, sizeof(xcecd_ctx));
xcecd_ctx.virtual_name = (char*)"pc13v";
vals = collect_load_indices(&count);
TEST_ASSERT_NOT_NULL(vals);
TEST_ASSERT_TRUE(count > 0);
for (i = 0; i < count; i++) {
TEST_ASSERT_NOT_NULL(vals[i].host);
TEST_ASSERT_EQUAL_STRING("pc13v", vals[i].host);
}
free_resvals(vals, count);
printf("test_collect_load_indices_tags_host_with_virtual_name_not_"
"gethostname: PASS\n");
}
static void
test_collect_load_indices_falls_back_to_gethostname_when_virtual_name_unset(
void)
{
xce_resval_t* vals;
int count = 0;
char expected[256];
memset(&xcecd_ctx, 0, sizeof(xcecd_ctx));
xcecd_ctx.virtual_name = NULL;
TEST_ASSERT_EQUAL_INT(0, gethostname(expected, sizeof(expected)));
vals = collect_load_indices(&count);
TEST_ASSERT_NOT_NULL(vals);
TEST_ASSERT_TRUE(count > 0);
TEST_ASSERT_EQUAL_STRING(expected, vals[0].host);
free_resvals(vals, count);
printf("test_collect_load_indices_falls_back_to_gethostname_when_"
"virtual_name_unset: 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);
return UNITY_END();
}

View file

@ -0,0 +1,375 @@
/*
* test_xcecd_startup_order.c — white-box test for the worker daemon's
* candidate-list startup ORDERING fix (component-failover-adoption, task
* 2.2, boundary: WorkerControllerConnector, Req 1.4).
*
* Bug fixed: main.c used to assign cc_ctx->cluster_cfg AFTER xcecd_start()
* had already returned -- but xcecd_start() is what creates cc_ctx
* (controller_connector_init()) AND fires the very first join
* (controller_connector_start()). So the first connect attempt always saw
* cc_ctx->cluster_cfg in its zero-initialized state (candidates_count == 0)
* and silently ignored any real --peers list on that first attempt.
*
* Fix: cluster_cfg is now threaded through xcecd_config_t (set by main.c
* BEFORE calling xcecd_init()) -> xcecd_context_t (copied by xcecd_init())
* -> cc_ctx->cluster_cfg (copied by controller_connector_init(), which
* xcecd_start() calls before controller_connector_start()).
*
* This test pulls the REAL xcecd_init()/xcecd_start()/controller_connector_
* init() production code into this TU (#include, same white-box pattern as
* test_controller_connector.c) and drives the real call sequence main.c ->
* xcecd_init() -> xcecd_start() uses, so a regression in the threading (or
* in xcecd_start()'s call ordering) would make this test fail exactly the
* way the pre-2.2 bug did.
*
* Why this is a SEPARATE file from test_controller_connector.c rather than
* an extension of it: xcecd.c and controller_connector.c cannot both be
* #include-d into the same translation unit. Both are white-box-included as
* whole files, and both would then try to provide real, conflicting
* definitions of shared symbols (xcecd_process_command is a real, non-static
* function in xcecd.c but is stubbed by hand in test_controller_connector.c
* for controller_connector.c's sake; cc_ctx is a real global controller_
* connector.c defines that xcecd.c only declares extern). This file instead
* stubs controller_connector.c's small exported surface that xcecd.c
* actually calls (start_controller_connector_thread, controller_connector_
* start, checking_connection, xcecd_job_start_error) and defines cc_ctx
* itself -- the same "link-satisfying stub" convention test_controller_
* connector.c already established, just pointed the other direction.
*
* Build flags: -DDBUS_MANAGER -DVIRTUAL_CLUSTER, the SAME flag pair the real
* `xcecd_virtual` production executable already uses (worker/meson.build) --
* not a new, never-exercised configuration. VIRTUAL_CLUSTER additionally
* skips xcecd_start()'s sqlite/db-recovery block and xcecd_process_command's
* dbus worker-launch block entirely (both `#ifndef VIRTUAL_CLUSTER`), which
* keeps this test's stub surface to the daemon's subsystem constructors
* (thread pool, process/system/hardware monitors, resource sync) rather than
* also needing a real or stubbed sqlite3/dbus stack.
*/
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
/* Pull the real xcecd_init()/xcecd_start()/controller_connector_init() logic
* under test into this TU. */
#include "../src/xcecd.c"
/* cc_ctx is normally defined in controller_connector.c (not included here --
* see file header comment for why). */
controller_connector_ctx_t* cc_ctx = NULL;
/* ════════════════════════════════════════════════════════════════════════
* Link-satisfying stubs
*
* Subsystem constructors (thread pool / process / system / hardware
* monitors / resource sync) return a non-NULL dummy pointer so xcecd_init()
* /xcecd_start() reach their real success paths -- this test cares about
* the cluster_cfg threading through those functions, not about hardware/
* process monitoring behavior. Nothing in this test's runtime path
* (xcecd_init() + xcecd_start(), never xcecd_cleanup() or
* xcecd_process_command()) dereferences these pointers, so a shared dummy
* buffer is sufficient and safe under ASAN.
* ════════════════════════════════════════════════════════════════════════ */
static char g_dummy_subsystem[4096];
thread_pool_t* thread_pool_create(size_t n)
{
(void)n;
return (thread_pool_t*)(void*)g_dummy_subsystem;
}
void thread_pool_destroy(thread_pool_t* pool) { (void)pool; }
process_manager_t* process_manager_create(void)
{
return (process_manager_t*)(void*)g_dummy_subsystem;
}
void process_manager_destroy(process_manager_t* manager) { (void)manager; }
bool process_manager_update(process_manager_t* manager)
{
(void)manager;
return true;
}
system_monitor_t* system_monitor_create(void)
{
return (system_monitor_t*)(void*)g_dummy_subsystem;
}
void system_monitor_destroy(system_monitor_t* monitor) { (void)monitor; }
bool system_monitor_update(system_monitor_t* monitor)
{
(void)monitor;
return true;
}
hardware_monitor_t* hardware_monitor_create(const char* acct_file)
{
(void)acct_file;
return (hardware_monitor_t*)(void*)g_dummy_subsystem;
}
void hardware_monitor_destroy(hardware_monitor_t* monitor) { (void)monitor; }
bool hardware_monitor_update(hardware_monitor_t* monitor)
{
(void)monitor;
return true;
}
int hardware_monitor_get_cpu_cores(hardware_monitor_t* monitor)
{
(void)monitor;
return 4;
}
double hardware_monitor_get_total_memory(hardware_monitor_t* monitor)
{
(void)monitor;
return 16384.0;
}
network_handler_t* network_handler_create(uint16_t port)
{
(void)port;
return (network_handler_t*)(void*)g_dummy_subsystem;
}
int network_handler_start(network_handler_t* handler)
{
(void)handler;
return 0;
}
void network_handler_destroy(network_handler_t* handler) { (void)handler; }
resource_sync_t* resource_sync_create(void)
{
return (resource_sync_t*)(void*)g_dummy_subsystem;
}
void resource_sync_destroy(resource_sync_t* sync) { (void)sync; }
bool resource_sync_start(resource_sync_t* sync)
{
(void)sync;
return true;
}
void resource_sync_stop(resource_sync_t* sync) { (void)sync; }
/* Referenced by xcecd_process_command()/xcecd_cleanup() -- never invoked at
* runtime by this test (only xcecd_init()/xcecd_start() are called), but
* their bodies are compiled unconditionally so the symbols must resolve. */
void add_worker(xcecd_worker_t** worker_list, int worker_pid,
int worker_status, uint64_t job_id, int ntasks,
job_startinfo_t* startinfo)
{
(void)worker_list;
(void)worker_pid;
(void)worker_status;
(void)job_id;
(void)ntasks;
(void)startinfo;
}
bool xcecd_db_insert_worker(xcecd_db_t* db, int worker_pid, int worker_status,
uint64_t job_id, int job_status)
{
(void)db;
(void)worker_pid;
(void)worker_status;
(void)job_id;
(void)job_status;
return true;
}
bool ipc_master_check_connection(void) { return true; }
pid_t dbus_get_worker_pid(const char* unit_name)
{
(void)unit_name;
return 1;
}
void dbus_stop_all_workers(xcecd_worker_t** worker_list, sqlite3* db)
{
(void)worker_list;
(void)db;
}
void cleanup_worker_list(xcecd_worker_t** worker_list, sqlite3* db)
{
(void)worker_list;
(void)db;
}
void xcecd_db_close(xcecd_db_t* db) { (void)db; }
void xcecd_db_is_available(xcecd_db_t* db, const char* db_path)
{
(void)db;
(void)db_path;
}
int submit_fork_task(thread_pool_t* pool, fork_task_t* task)
{
(void)pool;
(void)task;
return 0;
}
/* controller_connector.c's small exported surface xcecd.c actually calls
* (verified by grep -- start_controller_connector_thread,
* controller_connector_start, checking_connection, xcecd_job_start_error
* are the only 4). */
int xcecd_job_start_error(uint64_t jobid)
{
(void)jobid;
return 0;
}
int checking_connection(void) { return 0; }
/* controller_connector_start() is the function that fires the very first
* join attempt in production (xcecd_join_cluster() inside its retry loop).
* This test double stands in for it and snapshots cc_ctx->cluster_cfg at
* the EXACT moment production would fire that first attempt -- the core
* Req 1.4 assertion: was the real candidate list already there by then? */
static xce_cluster_config_t g_captured_cluster_cfg_at_first_join;
static bool g_controller_connector_start_called = false;
bool controller_connector_start(void)
{
g_controller_connector_start_called = true;
if (cc_ctx) {
g_captured_cluster_cfg_at_first_join = cc_ctx->cluster_cfg;
}
return true;
}
int start_controller_connector_thread(task_func_t function, void* arg)
{
/* Deliberately does NOT start a real thread -- connector_to_controller
* (which would call the real checking_connection() in a loop) must
* never actually run in this unit test. */
(void)function;
(void)arg;
return 0;
}
/* ════════════════════════════════════════════════════════════════════════
* Fixtures
* ════════════════════════════════════════════════════════════════════════ */
static xcecd_config_t base_test_config(void)
{
xcecd_config_t config = { 0 };
config.thread_pool_size = 1;
config.resource_update_interval = 1;
config.job_status_update_interval = 1;
config.port = 0; /* skip network_handler entirely */
config.acct_file = "/tmp/test_xcecd_startup_order.acct";
config.log_file = "/tmp/test_xcecd_startup_order.log";
config.db_path = "/tmp/test_xcecd_startup_order.db";
config.work_dir = "/tmp";
config.domain_sock_file = "/tmp/.test_xcecd_startup_order.sock";
config.virutal_name = "workertest";
config.remote_host = "localhost";
config.remote_port = 9990;
config.join_interval = 1;
config.update_interval = 1;
config.debug_level = 2;
return config;
}
void setUp(void)
{
memset(&xcecd_ctx, 0, sizeof(xcecd_ctx));
cc_ctx = NULL;
g_controller_connector_start_called = false;
memset(&g_captured_cluster_cfg_at_first_join, 0,
sizeof(g_captured_cluster_cfg_at_first_join));
}
void tearDown(void) { return; }
/* ════════════════════════════════════════════════════════════════════════
* Req 1.4 — cc_ctx->cluster_cfg is fully populated BEFORE the first join
* attempt fires, for a multi-candidate --peers config.
* ════════════════════════════════════════════════════════════════════════ */
static void
test_multi_candidate_cluster_cfg_populated_before_first_join(void)
{
xce_cluster_config_t peers_cfg = { 0 };
peers_cfg.candidates_count = 3;
strncpy(peers_cfg.candidates[0].hostname, "ctrl-a",
XCE_MAXHOSTNAME_LEN - 1);
peers_cfg.candidates[0].port = 9991;
strncpy(peers_cfg.candidates[1].hostname, "ctrl-b",
XCE_MAXHOSTNAME_LEN - 1);
peers_cfg.candidates[1].port = 9992;
strncpy(peers_cfg.candidates[2].hostname, "ctrl-c",
XCE_MAXHOSTNAME_LEN - 1);
peers_cfg.candidates[2].port = 9993;
strncpy(peers_cfg.identity, "workertest", sizeof(peers_cfg.identity) - 1);
xcecd_config_t config = base_test_config();
/* This is main.c's fix under test: cluster_cfg is set onto the config
* BEFORE xcecd_init()/xcecd_start() run (main.c:~430), not assigned onto
* cc_ctx afterward. */
config.cluster_cfg = peers_cfg;
TEST_ASSERT_EQUAL_INT(0, xcecd_init(&config));
TEST_ASSERT_EQUAL_INT(0, xcecd_start());
TEST_ASSERT_TRUE_MESSAGE(
g_controller_connector_start_called,
"controller_connector_start() (the first join attempt) must have "
"been invoked by xcecd_start()");
/* The core assertion: by the time the first join fires, cc_ctx-> */
/* cluster_cfg already holds the REAL 3-candidate --peers list -- not */
/* the zero-initialized/synthesized-single-candidate shape the pre-2.2 */
/* bug would have produced here. */
TEST_ASSERT_EQUAL_INT(
3, g_captured_cluster_cfg_at_first_join.candidates_count);
TEST_ASSERT_EQUAL_STRING(
"ctrl-a", g_captured_cluster_cfg_at_first_join.candidates[0].hostname);
TEST_ASSERT_EQUAL_INT(
9991, g_captured_cluster_cfg_at_first_join.candidates[0].port);
TEST_ASSERT_EQUAL_STRING(
"ctrl-b", g_captured_cluster_cfg_at_first_join.candidates[1].hostname);
TEST_ASSERT_EQUAL_INT(
9992, g_captured_cluster_cfg_at_first_join.candidates[1].port);
TEST_ASSERT_EQUAL_STRING(
"ctrl-c", g_captured_cluster_cfg_at_first_join.candidates[2].hostname);
TEST_ASSERT_EQUAL_INT(
9993, g_captured_cluster_cfg_at_first_join.candidates[2].port);
printf(
"test_multi_candidate_cluster_cfg_populated_before_first_join: "
"PASS\n");
}
/* Regression guard: the no-`--peers` case (cluster_cfg left {0}) must still
* reach the first join attempt with an EMPTY cc_ctx->cluster_cfg (candidates_
* count == 0) -- the synthesize-single-candidate path inside build_cluster_
* config() (controller_connector.c, task 2.1) owns turning that into a
* one-candidate list; this ordering fix must not accidentally force a
* candidate in cc_ctx->cluster_cfg when none was configured. */
static void test_no_peers_cluster_cfg_stays_empty_before_first_join(void)
{
xcecd_config_t config = base_test_config();
/* config.cluster_cfg left {0} -- no --peers given. */
TEST_ASSERT_EQUAL_INT(0, xcecd_init(&config));
TEST_ASSERT_EQUAL_INT(0, xcecd_start());
TEST_ASSERT_TRUE(g_controller_connector_start_called);
TEST_ASSERT_EQUAL_INT(
0, g_captured_cluster_cfg_at_first_join.candidates_count);
printf(
"test_no_peers_cluster_cfg_stays_empty_before_first_join: PASS\n");
}
/* ════════════════════════════════════════════════════════════════════════
* main
* ════════════════════════════════════════════════════════════════════════ */
int main(void)
{
xce_init_xcecd_log("/tmp/test_xcecd_startup_order.log", 1024 * 1024, 2);
UNITY_BEGIN();
RUN_TEST(test_multi_candidate_cluster_cfg_populated_before_first_join);
RUN_TEST(test_no_peers_cluster_cfg_stays_empty_before_first_join);
return UNITY_END();
}