Arm a reconciliation round in apply_join_state() the moment a worker host becomes connected (first join, reconnect, or failover-reconnect - all routed through the same shared call site, Req 1.1-1.3), then send the query from on_worker_join() strictly after the join reply and existing config push (Req 2.1-2.2), verified via a structural test that reads real wire order off the endpoint queue. Also closes Req 3.7's config-delivery seam end-to-end: worker_resync_ window_ms now rides the existing Conf4Worker push (clustername precedent, no new opcode) and the worker applies it to its retention window on receipt. Fixes bug-1752: myreq_handler had no OPCode_push dispatch case at all, so every config push - including the pre-existing clustername field - was silently discarded. Confirmed harmless until now (clustername was never read anywhere in src/worker/); closed narrowly for worker_resync_window_ms only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y4M5MJM2zK6nU43da267Ku
2080 lines
74 KiB
C
Executable file
2080 lines
74 KiB
C
Executable file
#include "client.h"
|
|
#include "controller_connector.h"
|
|
#include "netio.h"
|
|
#include "uv_ipc.h"
|
|
#include "xcecd_cgroups.h"
|
|
#include "xcecd_exdata.h"
|
|
#include "xcecd_resync.h"
|
|
#include "xcecd_worker_utils.h"
|
|
#include "xcelog.h"
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#else
|
|
#include <pthread.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#ifdef XRUN
|
|
hostattrs_t host_attrs = { 0 };
|
|
#else
|
|
extern hostattrs_t host_attrs;
|
|
#endif
|
|
|
|
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 */
|
|
typedef struct {
|
|
task_func_t function;
|
|
void* arg;
|
|
} thread_wrapper_t;
|
|
|
|
static const cc_callbacks_t cc_callbacks
|
|
= { .job_finished = xcecd_job_finished,
|
|
.job_stopped = xcecd_job_stopped,
|
|
.job_resumed = xcecd_job_resumed,
|
|
.job_start_error = xcecd_job_start_error,
|
|
.job_status_error = xcecd_job_status_error,
|
|
.job_ckpted = xcecd_job_ckpted,
|
|
.job_remote_start = xcecd_job_remote_start,
|
|
.reply_startjob = xcecd_reply_startjob };
|
|
|
|
static void free_startinfo(job_startinfo_t* info)
|
|
{
|
|
if (!info) {
|
|
return;
|
|
}
|
|
FREEUP(info->cmdline);
|
|
FREEUP(info->execuser);
|
|
FREEUP(info->exdata);
|
|
FREEUP(info->exechome);
|
|
FREEUP(info->execcwd);
|
|
FREEUP(info->q_pre_exec);
|
|
FREEUP(info->q_post_exec);
|
|
FREEUP(info->job_pre_exec);
|
|
FREEUP(info->job_post_exec);
|
|
FREEUP(info);
|
|
}
|
|
|
|
static void free_limits(res_limits_t* limits)
|
|
{
|
|
if (!limits) {
|
|
return;
|
|
}
|
|
FREEUP(limits->cpu_affinity);
|
|
FREEUP(limits->io_device_path);
|
|
FREEUP(limits->user_name);
|
|
FREEUP(limits);
|
|
}
|
|
|
|
/**
|
|
* @brief Initializes the host attributes with default values.
|
|
*/
|
|
const cc_callbacks_t* get_cc_callbacks(void) { return &cc_callbacks; }
|
|
|
|
static void* thread_start_wrapper(void* arg)
|
|
{
|
|
thread_wrapper_t* wrapper = (thread_wrapper_t*)arg;
|
|
wrapper->function(wrapper->arg);
|
|
FREEUP(wrapper);
|
|
return NULL;
|
|
}
|
|
|
|
/* my old implementation and currently is disabled */
|
|
int start_controller_connector_thread(task_func_t function, void* arg)
|
|
{
|
|
XCECD_LOG(XCE_LOG_DEBUG, "Starting controller connector thread...");
|
|
#ifdef _WIN32
|
|
HANDLE status_thread
|
|
= CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)function, arg, 0, NULL);
|
|
if (status_thread == NULL) {
|
|
return -1;
|
|
}
|
|
CloseHandle(status_thread);
|
|
#else
|
|
pthread_t status_thread;
|
|
thread_wrapper_t* wrapper = calloc(1, sizeof(thread_wrapper_t));
|
|
if (!wrapper) {
|
|
return -1;
|
|
}
|
|
wrapper->function = function;
|
|
wrapper->arg = arg;
|
|
if (pthread_create(&status_thread, NULL, thread_start_wrapper, wrapper)
|
|
!= 0) {
|
|
FREEUP(wrapper);
|
|
return -1;
|
|
}
|
|
pthread_detach(status_thread);
|
|
#endif
|
|
|
|
XCECD_LOG(XCE_LOG_DEBUG, "Controller connector thread started.");
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @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 update.
|
|
*/
|
|
static void set_client_identity(xce_client_config_t* config)
|
|
{
|
|
int ret;
|
|
char local_host[MAXHOSTNAMELEN] = { 0 };
|
|
|
|
if (!config) {
|
|
return;
|
|
}
|
|
|
|
ret = gethostname(local_host, sizeof(local_host));
|
|
if (ret < 0) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "gethostname failed: %s", strerror(errno));
|
|
strncpy(local_host, "localhost", sizeof(local_host) - 1);
|
|
}
|
|
|
|
#ifdef VIRTUAL_CLUSTER
|
|
config->from = strdup(cc_ctx->virtual_name);
|
|
#else
|
|
config->from = strdup(local_host);
|
|
#endif
|
|
config->key = strdup("keepSecret");
|
|
|
|
XCECD_LOG(XCE_LOG_DEBUG,
|
|
"Controller connector identity set: from=%s", config->from);
|
|
}
|
|
|
|
/**
|
|
* @brief Build the single validated candidate list this connect/reconnect
|
|
* attempt will use (component-failover-adoption, task 2.1, Req 1
|
|
* AC5).
|
|
*
|
|
* 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 on a validated config, the validator's negative error code
|
|
* (or -EINVAL if cc_ctx is unset) otherwise.
|
|
*/
|
|
static int build_cluster_config(void)
|
|
{
|
|
if (!cc_ctx) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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 flags/statics and
|
|
* returns 0 immediately so the interrupted operation resumes normally.
|
|
* Consuming needs_rejoin 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.
|
|
*
|
|
* worker-rejoin-resync task 4.3 remediation (round 2, Requirements 3.8,
|
|
* 6.1): also resets xcecd_resync.c's resync_pending gate here. This is the
|
|
* moment the worker itself learns its connection to the controller was
|
|
* re-established -- any job-state query it was still assembling (or report
|
|
* it had not finished sending) belongs to the connection that just died and
|
|
* can never complete; the controller's own Requirement 6.1 Acceptance
|
|
* Criterion 1 already discards that incomplete round and re-initiates a
|
|
* fresh query on this worker's next rejoin. Without this reset,
|
|
* resync_pending can only be cleared by xcecd_resync_handle_query_page()
|
|
* seeing a query's LAST page -- which never arrives on a dead connection --
|
|
* leaving it stuck true forever (round 1's confirmed defect). Calling
|
|
* xcecd_resync_reset() here is safe under the constraint above: it only
|
|
* touches xcecd_resync.c's own statics (a bool and a small heap buffer), no
|
|
* I/O and no xcecli_* call.
|
|
*/
|
|
static int xcecd_on_reconnect(xcecli_ctx_t ctx, void* arg)
|
|
{
|
|
(void)ctx;
|
|
(void)arg;
|
|
|
|
needs_rejoin = true;
|
|
xcecd_resync_reset();
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @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.
|
|
*
|
|
* worker-rejoin-resync task 4.3 (Requirement 3.8), narrowed per round-2
|
|
* remediation: xcecd_resync_is_pending() gates ONLY the needs_rejoin-
|
|
* triggered rejoin_call() below -- the "resume normal post-join operation"
|
|
* step Requirement 3.8 actually names -- not this function's entry.
|
|
* xcecli_hello() and the needs_rejoin check itself both still run every
|
|
* tick unconditionally, so checking_connection() keeps the exact
|
|
* self-healing property needs_rejoin already had before this task: a fresh
|
|
* chance every cycle to detect and recover the worker's own connection
|
|
* (src/lib/xcectx.c's handle_disconnect() -- "so that checking_connection
|
|
* can detect the failure and attempt to rejoin"). A resync_pending stuck
|
|
* true (round 1's confirmed defect: gating this function's entry made the
|
|
* worker's sole connection-recovery mechanism permanently inert, breaking
|
|
* Requirement 6.1's "disconnect during an incomplete reconciliation round"
|
|
* case) can therefore never disable this probe. In practice resync_pending
|
|
* is also cleared the instant a reconnect is detected
|
|
* (xcecd_on_reconnect() -> xcecd_resync_reset(), above), so this check is a
|
|
* defensive backstop against the rejoin racing an in-flight report, not the
|
|
* mechanism that unsticks a stalled one.
|
|
*
|
|
* @return int Always returns 0.
|
|
*/
|
|
int checking_connection()
|
|
{
|
|
int ret = xcecli_hello(xcecli_ctx, "hello");
|
|
|
|
if (ret != 0) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Connection to controller not currently healthy (rc=%d); "
|
|
"the client library will keep retrying/rotating "
|
|
"candidates in the background",
|
|
ret);
|
|
}
|
|
|
|
if (needs_rejoin) {
|
|
if (xcecd_resync_is_pending()) {
|
|
XCECD_LOG(XCE_LOG_DEBUG,
|
|
"checking_connection: deferring rejoin -- job-state "
|
|
"resync report still being sent");
|
|
} else {
|
|
int rc = rejoin_call();
|
|
if (rc >= 0) {
|
|
needs_rejoin = false;
|
|
XCECD_LOG(XCE_LOG_INFO,
|
|
"checking_connection: rejoin after transparent "
|
|
"reconnect succeeded");
|
|
} else {
|
|
XCECD_LOG(XCE_LOG_WARN,
|
|
"checking_connection: rejoin after transparent "
|
|
"reconnect failed (rc=%d); will retry on the next "
|
|
"liveness probe",
|
|
rc);
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#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: %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");
|
|
}
|
|
#endif // XRUN
|
|
|
|
static char* create_mpi_cmdline(char* cmd, char* type, xce_launch_t* launchinfo)
|
|
{
|
|
if (!cmd || !type || !launchinfo || launchinfo->nhosts <= 0) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Invalid parameters for create_mpi_cmdline");
|
|
return NULL;
|
|
}
|
|
|
|
char* mpi_executable = NULL;
|
|
char* app_command = NULL;
|
|
char* token = NULL;
|
|
char* cmd_copy = strdup(cmd);
|
|
if (!cmd_copy) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to allocate memory for cmd_copy");
|
|
return NULL;
|
|
}
|
|
|
|
// Parse the original command to extract mpi executable path and application
|
|
// command
|
|
token = strtok(cmd_copy, " ");
|
|
if (!token) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to parse command: %s", cmd);
|
|
FREEUP(cmd_copy);
|
|
return NULL;
|
|
}
|
|
|
|
// Check if first token contains mpirun or mpiexec
|
|
if (strstr(token, "mpirun") || strstr(token, "mpiexec")) {
|
|
mpi_executable = strdup(token);
|
|
// Get the rest of the command as application
|
|
char* rest = cmd + strlen(token);
|
|
while (*rest == ' ')
|
|
rest++; // skip spaces
|
|
app_command = strdup(rest);
|
|
} else {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Command does not contain mpirun or mpiexec: %s", cmd);
|
|
FREEUP(cmd_copy);
|
|
return NULL;
|
|
}
|
|
|
|
FREEUP(cmd_copy);
|
|
|
|
if (!mpi_executable || !app_command) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Failed to extract mpi executable or application command");
|
|
FREEUP(mpi_executable);
|
|
FREEUP(app_command);
|
|
return NULL;
|
|
}
|
|
|
|
// Calculate the size needed for host list
|
|
size_t hostlist_size = 0;
|
|
for (int i = 0; i < launchinfo->nhosts; i++) {
|
|
if (launchinfo->hosts[i] && launchinfo->hosts[i]->host) {
|
|
// host:nrank + comma + null terminator
|
|
hostlist_size += strlen(launchinfo->hosts[i]->host)
|
|
+ 20; // 20 should be enough for :nrank
|
|
}
|
|
}
|
|
|
|
if (hostlist_size == 0) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "No valid hosts found in launch info");
|
|
FREEUP(mpi_executable);
|
|
FREEUP(app_command);
|
|
return NULL;
|
|
}
|
|
|
|
// Build host list string
|
|
char* hostlist = calloc(hostlist_size, sizeof(char));
|
|
if (!hostlist) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to allocate memory for hostlist");
|
|
FREEUP(mpi_executable);
|
|
FREEUP(app_command);
|
|
return NULL;
|
|
}
|
|
|
|
for (int i = 0; i < launchinfo->nhosts; i++) {
|
|
if (launchinfo->hosts[i] && launchinfo->hosts[i]->host) {
|
|
if (strlen(hostlist) > 0) {
|
|
strcat(hostlist, ",");
|
|
}
|
|
strcat(hostlist, launchinfo->hosts[i]->host);
|
|
strcat(hostlist, ":");
|
|
|
|
char nrank_str[16];
|
|
snprintf(nrank_str, sizeof(nrank_str), "%d",
|
|
launchinfo->hosts[i]->nranks);
|
|
strcat(hostlist, nrank_str);
|
|
}
|
|
}
|
|
|
|
char launcher[MAXFILENAMELEN] = { 0 };
|
|
char* server_dir = getenv("XCE_SERVER_DIR");
|
|
if (server_dir) {
|
|
snprintf(launcher, sizeof(launcher), "%s/xrun", server_dir);
|
|
} else {
|
|
snprintf(launcher, sizeof(launcher), "/opt/xce/bin/xrun");
|
|
}
|
|
|
|
if (access(launcher, X_OK) != 0) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"xrun launcher not found or not executable at %s", launcher);
|
|
FREEUP(mpi_executable);
|
|
FREEUP(app_command);
|
|
FREEUP(hostlist);
|
|
return NULL;
|
|
}
|
|
|
|
// Determine the host parameter based on MPI type
|
|
const char* host_param = NULL;
|
|
const char* launcher_param = NULL;
|
|
if (strcmp(type, "openmpi") == 0) {
|
|
host_param = "-host";
|
|
launcher_param = "--mca plm_rsh_agent";
|
|
} else if (strcmp(type, "mpich") == 0) {
|
|
host_param = "-hosts";
|
|
launcher_param = "-launcher-exec";
|
|
} else {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Unsupported MPI type: %s", type);
|
|
FREEUP(mpi_executable);
|
|
FREEUP(app_command);
|
|
FREEUP(hostlist);
|
|
return NULL;
|
|
}
|
|
|
|
// Calculate final command size
|
|
size_t final_cmd_size
|
|
= strlen(mpi_executable) + 1 + // executable + space
|
|
strlen(host_param) + 1 + // host parameter + space
|
|
strlen(hostlist) + 1 + // hostlist + space
|
|
strlen(launcher_param) + 1 + // launcher parameter + space
|
|
strlen(launcher) + 1 + // launcher + space
|
|
strlen(app_command) + 1; // application + null terminator
|
|
|
|
char* final_cmd = calloc(final_cmd_size, sizeof(char));
|
|
if (!final_cmd) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to allocate memory for final command");
|
|
FREEUP(mpi_executable);
|
|
FREEUP(app_command);
|
|
FREEUP(hostlist);
|
|
return NULL;
|
|
}
|
|
|
|
// Build the final command
|
|
snprintf(final_cmd, final_cmd_size, "%s %s %s %s %s %s", mpi_executable,
|
|
host_param, hostlist, launcher_param, launcher, app_command);
|
|
|
|
XCECD_LOG(XCE_LOG_DEBUG3, "Generated MPI command: %s", final_cmd);
|
|
|
|
// Cleanup
|
|
FREEUP(mpi_executable);
|
|
FREEUP(app_command);
|
|
FREEUP(hostlist);
|
|
|
|
return final_cmd;
|
|
}
|
|
|
|
/**
|
|
* @brief Runs a job task on remote hosts.
|
|
*
|
|
* @param jobid The ID of the job to run.
|
|
* @param startinfo Pointer to the job start information.
|
|
* @param launchinfo Pointer to the job launch information.
|
|
* @return int 0 on success, -1 on failure.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
if ((strcmp(startinfo->launch_type, "openmpi") == 0)
|
|
|| (strcmp(startinfo->launch_type, "mpich") == 0)) {
|
|
XCECD_LOG(XCE_LOG_DEBUG3,
|
|
"MPI launch detected, using MPI task run method.");
|
|
return 0;
|
|
}
|
|
|
|
for (i = 1; i < launchinfo->nhosts; i++) {
|
|
hosts[i - 1].exechome = startinfo->exechome;
|
|
hosts[i - 1].execcwd = startinfo->execcwd;
|
|
hosts[i - 1].cmdline = startinfo->cmdline;
|
|
hosts[i - 1].exdata = startinfo->exdata;
|
|
hosts[i - 1].execuser = startinfo->execuser;
|
|
hosts[i - 1].launch_type = startinfo->launch_type;
|
|
|
|
hosts[i - 1].host = launchinfo->hosts[i]->host;
|
|
hosts[i - 1].ntasks = launchinfo->hosts[i]->nranks;
|
|
|
|
XCECD_LOG(XCE_LOG_DEBUG3, "Remote task setup for host %s with %d ranks",
|
|
hosts[i - 1].host, hosts[i - 1].ntasks);
|
|
}
|
|
|
|
taskrun.taskhosts = (xce_taskhost_t**)calloc(launchinfo->nhosts - 1,
|
|
sizeof(xce_taskhost_t*));
|
|
taskrun.clientidx = -1;
|
|
|
|
if (taskrun.taskhosts != NULL) {
|
|
for (i = 1; i < launchinfo->nhosts; i++) {
|
|
taskrun.taskhosts[i - 1] = &hosts[i - 1];
|
|
}
|
|
|
|
taskrun.nhosts = launchinfo->nhosts - 1;
|
|
taskrun.jobid = 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 %s run remote task failed.", jobidStr);
|
|
}
|
|
} else {
|
|
rc = -ENOMEM;
|
|
}
|
|
|
|
FREEUP(taskrun.taskhosts);
|
|
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* @brief Starts a job task on the local host.
|
|
*
|
|
* @param request Pointer to the xce_request_t structure containing the request
|
|
* data.
|
|
* @return int 0 on success, -1 on failure.
|
|
*/
|
|
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);
|
|
|
|
fork_task_t* job_ctx = calloc(1, sizeof(fork_task_t));
|
|
if (job_ctx) {
|
|
job_ctx->startinfo = calloc(1, sizeof(job_startinfo_t));
|
|
if (!job_ctx->startinfo) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to allocate memory for startinfo");
|
|
xcecd_job_start_error(d->jobid);
|
|
FREEUP(job_ctx);
|
|
return -1;
|
|
}
|
|
|
|
if (d->start_info->execuser && d->start_info->execuser[0] != '\0') {
|
|
job_ctx->startinfo->execuser
|
|
= strdup((char*)d->start_info->execuser);
|
|
} else {
|
|
job_ctx->startinfo->execuser = strdup("nobody");
|
|
XCECD_LOG(XCE_LOG_WARN, "No execuser provided in startjob request, "
|
|
"defaulting to 'nobody'");
|
|
}
|
|
|
|
if (!job_ctx->startinfo->execuser) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to allocate memory for execuser");
|
|
xcecd_job_start_error(d->jobid);
|
|
free_startinfo(job_ctx->startinfo);
|
|
FREEUP(job_ctx);
|
|
return -1;
|
|
}
|
|
|
|
#if 0
|
|
char data[1024] = {0};
|
|
snprintf(data, sizeof(data),
|
|
"{\"sub_env_vars\": {\"XCE_TEST1\": \"abab\", \"XCE_TEST2\": \"cdcd\"}, \"transfer_file\": [{\"op\": \">\", \"local_file\": \"/tmp/file1.txt\", \"remote_file\": \"\"}, {\"op\": \">\", \"local_file\": \"/tmp/file2.txt\", \"remote_file\": \"\"}], \"interactive\": 1}");
|
|
job_ctx->exdata = strdup(data);
|
|
#else
|
|
|
|
if (!d->start_info->exdata || d->start_info->exdata[0] == '\0') {
|
|
job_ctx->startinfo->exdata = NULL;
|
|
XCECD_LOG(XCE_LOG_DEBUG3, "No exdata provided in startjob request");
|
|
} else {
|
|
job_ctx->startinfo->exdata = strdup((char*)d->start_info->exdata);
|
|
XCECD_LOG(XCE_LOG_DEBUG3, "Received exdata: %s",
|
|
job_ctx->startinfo->exdata);
|
|
|
|
if (!job_ctx->startinfo->exdata) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Failed to allocate memory for exdata");
|
|
xcecd_job_start_error(d->jobid);
|
|
free_startinfo(job_ctx->startinfo);
|
|
FREEUP(job_ctx);
|
|
return -1;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (!d->start_info->exechome || d->start_info->exechome[0] == '\0') {
|
|
job_ctx->startinfo->exechome = NULL;
|
|
XCECD_LOG(XCE_LOG_DEBUG3,
|
|
"No exechome provided in startjob request");
|
|
} else {
|
|
job_ctx->startinfo->exechome
|
|
= strdup((char*)d->start_info->exechome);
|
|
if (!job_ctx->startinfo->exechome) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Failed to allocate memory for exechome");
|
|
xcecd_job_start_error(d->jobid);
|
|
free_startinfo(job_ctx->startinfo);
|
|
FREEUP(job_ctx);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (!d->start_info->execcwd || d->start_info->execcwd[0] == '\0') {
|
|
job_ctx->startinfo->execcwd = NULL;
|
|
XCECD_LOG(XCE_LOG_DEBUG3,
|
|
"No execcwd provided in startjob request");
|
|
} else {
|
|
job_ctx->startinfo->execcwd = strdup((char*)d->start_info->execcwd);
|
|
if (!job_ctx->startinfo->execcwd) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Failed to allocate memory for execcwd");
|
|
xcecd_job_start_error(d->jobid);
|
|
free_startinfo(job_ctx->startinfo);
|
|
FREEUP(job_ctx);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (!d->start_info->q_pre_exec
|
|
|| d->start_info->q_pre_exec[0] == '\0') {
|
|
job_ctx->startinfo->q_pre_exec = NULL;
|
|
XCECD_LOG(XCE_LOG_DEBUG3,
|
|
"No queue level pre-exec provided in startjob request");
|
|
} else {
|
|
job_ctx->startinfo->q_pre_exec
|
|
= strdup((char*)d->start_info->q_pre_exec);
|
|
if (!job_ctx->startinfo->q_pre_exec) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to allocate memory for queue "
|
|
"level pre-exec command.");
|
|
xcecd_job_start_error(d->jobid);
|
|
free_startinfo(job_ctx->startinfo);
|
|
FREEUP(job_ctx);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (!d->start_info->q_post_exec
|
|
|| d->start_info->q_post_exec[0] == '\0') {
|
|
job_ctx->startinfo->q_post_exec = NULL;
|
|
XCECD_LOG(XCE_LOG_DEBUG3,
|
|
"No queue level post-exec provided in startjob request");
|
|
} else {
|
|
job_ctx->startinfo->q_post_exec
|
|
= strdup((char*)d->start_info->q_post_exec);
|
|
if (!job_ctx->startinfo->q_post_exec) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to allocate memory for queue "
|
|
"level post-exec command.");
|
|
xcecd_job_start_error(d->jobid);
|
|
free_startinfo(job_ctx->startinfo);
|
|
FREEUP(job_ctx);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (!d->start_info->job_pre_exec
|
|
|| d->start_info->job_pre_exec[0] == '\0') {
|
|
job_ctx->startinfo->job_pre_exec = NULL;
|
|
XCECD_LOG(XCE_LOG_DEBUG3,
|
|
"No job level pre-exec provided in startjob request");
|
|
} else {
|
|
job_ctx->startinfo->job_pre_exec
|
|
= strdup((char*)d->start_info->job_pre_exec);
|
|
if (!job_ctx->startinfo->job_pre_exec) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to allocate memory for job "
|
|
"level pre-exec command.");
|
|
xcecd_job_start_error(d->jobid);
|
|
free_startinfo(job_ctx->startinfo);
|
|
FREEUP(job_ctx);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (!d->start_info->job_post_exec
|
|
|| d->start_info->job_post_exec[0] == '\0') {
|
|
job_ctx->startinfo->job_post_exec = NULL;
|
|
XCECD_LOG(XCE_LOG_DEBUG3,
|
|
"No job level post-exec provided in startjob request");
|
|
} else {
|
|
job_ctx->startinfo->job_post_exec
|
|
= strdup((char*)d->start_info->job_post_exec);
|
|
if (!job_ctx->startinfo->job_post_exec) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to allocate memory for job "
|
|
"level post-exec command.");
|
|
xcecd_job_start_error(d->jobid);
|
|
free_startinfo(job_ctx->startinfo);
|
|
FREEUP(job_ctx);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
#ifndef DBUS_MANAGER
|
|
job_ctx->manager = (void*)cc_ctx->process_manager;
|
|
#endif
|
|
|
|
if (d->start_info->launch_type) {
|
|
job_ctx->startinfo->launch_type
|
|
= strdup((char*)d->start_info->launch_type);
|
|
} else {
|
|
job_ctx->startinfo->launch_type = NULL;
|
|
}
|
|
|
|
job_ctx->func = xcecd_process_command;
|
|
job_ctx->xce_jobid = d->jobid;
|
|
job_ctx->ntasks = d->launchinfo->hosts[0]->nranks;
|
|
job_ctx->clientidx = -1;
|
|
|
|
/* Create MPI command line */
|
|
if (strcmp(job_ctx->startinfo->launch_type, "openmpi") == 0
|
|
|| strcmp(job_ctx->startinfo->launch_type, "mpich") == 0) {
|
|
job_ctx->startinfo->cmdline
|
|
= create_mpi_cmdline(d->start_info->cmdline,
|
|
d->start_info->launch_type, d->launchinfo);
|
|
if (job_ctx->startinfo->cmdline == NULL) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to create MPI command line");
|
|
xcecd_job_start_error(d->jobid);
|
|
free_startinfo(job_ctx->startinfo);
|
|
FREEUP(job_ctx);
|
|
return -1;
|
|
}
|
|
|
|
/* In MPI case, we set ntasks to 1 on all nodes as we have passed
|
|
* the host options command line to all of them.
|
|
*/
|
|
job_ctx->ntasks = 1;
|
|
} else {
|
|
job_ctx->startinfo->cmdline = strdup((char*)d->start_info->cmdline);
|
|
if (!job_ctx->startinfo->cmdline) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Failed to allocate memory for command");
|
|
xcecd_job_start_error(d->jobid);
|
|
free_startinfo(job_ctx->startinfo);
|
|
FREEUP(job_ctx);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
fmt_jobid(jobidStr, sizeof(jobidStr), d->jobid);
|
|
XCECD_LOG(XCE_LOG_DEBUG, "Job %s ntasks is: %d", jobidStr,
|
|
job_ctx->ntasks);
|
|
|
|
/* Job limits */
|
|
job_ctx->limits = (res_limits_t*)calloc(1, sizeof(res_limits_t));
|
|
if (!job_ctx->limits) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Failed to allocate memory for job limits");
|
|
xcecd_job_start_error(d->jobid);
|
|
free_startinfo(job_ctx->startinfo);
|
|
FREEUP(job_ctx);
|
|
return -1;
|
|
}
|
|
|
|
/* Even exdata is empty, we still need to initialize limits as for
|
|
* runtime user */
|
|
if (!parse_limits(job_ctx->startinfo->exdata, job_ctx->limits)) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to parse job limits from exdata");
|
|
xcecd_job_start_error(d->jobid);
|
|
free_startinfo(job_ctx->startinfo);
|
|
free_limits(job_ctx->limits);
|
|
FREEUP(job_ctx);
|
|
return -1;
|
|
}
|
|
|
|
if (job_ctx->startinfo->execuser != NULL) {
|
|
job_ctx->limits->user_name = strdup(job_ctx->startinfo->execuser);
|
|
} else {
|
|
job_ctx->limits->user_name = strdup("nobody");
|
|
XCECD_LOG(XCE_LOG_WARN,
|
|
"No execuser provided, defaulting to 'nobody'");
|
|
}
|
|
|
|
job_ctx->limits->cpu_affinity = NULL;
|
|
job_ctx->limits->io_device_path = NULL;
|
|
|
|
if (submit_fork_task(cc_ctx->thread_pool, job_ctx) != 0) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to add task to thread pool");
|
|
|
|
xcecd_job_start_error(d->jobid);
|
|
free_startinfo(job_ctx->startinfo);
|
|
free_limits(job_ctx->limits);
|
|
FREEUP(job_ctx);
|
|
return -1;
|
|
} else {
|
|
XCECD_LOG(XCE_LOG_DEBUG, "Task added to thread pool queue");
|
|
}
|
|
} else {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to allocate memory for task context");
|
|
xcecd_job_start_error(d->jobid);
|
|
return -1;
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
static int handle_startjob(xce_request_t* request)
|
|
{
|
|
int rc = 0;
|
|
char jobidStr[64];
|
|
|
|
XCECD_LOG(XCE_LOG_DEBUG, "Received startjob request from controller");
|
|
|
|
if (request->body.jobstartmsg == NULL
|
|
|| request->body.jobstartmsg->start_info == NULL
|
|
|| request->body.jobstartmsg->launchinfo == NULL) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Received startjob request with invalid parameters");
|
|
return -1;
|
|
}
|
|
|
|
if (request->body.jobstartmsg->jobid <= 0) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Received startjob request with invalid job ID");
|
|
return -1;
|
|
}
|
|
|
|
uint64_t jobid = request->body.jobstartmsg->jobid;
|
|
|
|
if (request->body.jobstartmsg->start_info->cmdline == NULL) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Received startjob request with empty command");
|
|
xcecd_job_start_error(jobid);
|
|
return -1;
|
|
}
|
|
|
|
if (request->body.jobstartmsg->start_info->execuser == NULL) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Received startjob request with empty execuser");
|
|
xcecd_job_start_error(jobid);
|
|
return -1;
|
|
}
|
|
|
|
job_startinfo_t* startinfo = request->body.jobstartmsg->start_info;
|
|
xce_launch_t* launchinfo = request->body.jobstartmsg->launchinfo;
|
|
|
|
if (launchinfo->nhosts > 1) {
|
|
rc = job_start_remote_task(jobid, startinfo, launchinfo);
|
|
if (rc < 0) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
rc = job_start_local_task(request);
|
|
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* @brief Handles the runtasks request from the controller.
|
|
*
|
|
* @param request Pointer to the xce_request_t structure containing the request
|
|
* data.
|
|
* @return int 0 on success, -1 on failure.
|
|
*/
|
|
static int handle_runtasks(xce_request_t* request)
|
|
{
|
|
#ifndef XRUN
|
|
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,
|
|
"Received runtasks request with invalid message");
|
|
return -1;
|
|
}
|
|
|
|
if (runmsg->jobid <= 0) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Received runtasks request with invalid job ID");
|
|
return -1;
|
|
}
|
|
|
|
if (runmsg->nhosts != 1) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Received runtasks request with invalid number of hosts: %d",
|
|
runmsg->nhosts);
|
|
xcecd_task_fail(runmsg->jobid, runmsg->clientidx, -1, -EINVAL);
|
|
return -1;
|
|
}
|
|
|
|
if (runmsg->taskhosts[0]->host == NULL) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Received runtasks request with empty host name");
|
|
return -1;
|
|
}
|
|
|
|
if (strcmp(xce_client_cfg.from, runmsg->taskhosts[0]->host) != 0) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Received runtasks request for a different host: %s",
|
|
runmsg->taskhosts[0]->host);
|
|
xcecd_task_fail(runmsg->jobid, runmsg->clientidx, -1, -EINVAL);
|
|
return -1;
|
|
}
|
|
|
|
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");
|
|
XCECD_LOG(XCE_LOG_DEBUG3, "run task: cmdline = %s",
|
|
runmsg->taskhosts[0]->cmdline ? runmsg->taskhosts[0]->cmdline
|
|
: "NULL");
|
|
XCECD_LOG(XCE_LOG_DEBUG3, "run task: exdata = %s",
|
|
runmsg->taskhosts[0]->exdata ? runmsg->taskhosts[0]->exdata
|
|
: "NULL");
|
|
XCECD_LOG(XCE_LOG_DEBUG3, "run task: ntasks = %d",
|
|
runmsg->taskhosts[0]->ntasks);
|
|
XCECD_LOG(XCE_LOG_DEBUG3, "run task: exechome = %s",
|
|
runmsg->taskhosts[0]->exechome ? runmsg->taskhosts[0]->exechome
|
|
: "NULL");
|
|
XCECD_LOG(XCE_LOG_DEBUG3, "run task: execcwd = %s",
|
|
runmsg->taskhosts[0]->execcwd ? runmsg->taskhosts[0]->execcwd
|
|
: "NULL");
|
|
|
|
fork_task_t* task_ctx = calloc(1, sizeof(fork_task_t));
|
|
if (task_ctx) {
|
|
task_ctx->startinfo = calloc(1, sizeof(job_startinfo_t));
|
|
if (!task_ctx->startinfo) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to allocate memory for startinfo");
|
|
FREEUP(task_ctx);
|
|
return -1;
|
|
}
|
|
|
|
task_ctx->startinfo->cmdline
|
|
= strdup((char*)runmsg->taskhosts[0]->cmdline);
|
|
if (!task_ctx->startinfo->cmdline) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to allocate memory for command");
|
|
free_startinfo(task_ctx->startinfo);
|
|
FREEUP(task_ctx);
|
|
xcecd_task_fail(runmsg->jobid, runmsg->clientidx, -1, -ENOMEM);
|
|
return -1;
|
|
}
|
|
|
|
task_ctx->startinfo->execuser
|
|
= strdup((char*)runmsg->taskhosts[0]->execuser);
|
|
if (!task_ctx->startinfo->execuser) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to allocate memory for execuser");
|
|
free_startinfo(task_ctx->startinfo);
|
|
FREEUP(task_ctx);
|
|
xcecd_task_fail(runmsg->jobid, runmsg->clientidx, -1, -ENOMEM);
|
|
return -1;
|
|
}
|
|
|
|
task_ctx->ntasks = runmsg->taskhosts[0]->ntasks;
|
|
if (task_ctx->ntasks <= 0) {
|
|
XCECD_LOG(
|
|
XCE_LOG_ERROR,
|
|
"Received runtasks request with invalid number of tasks: %d",
|
|
task_ctx->ntasks);
|
|
free_startinfo(task_ctx->startinfo);
|
|
FREEUP(task_ctx);
|
|
xcecd_task_fail(runmsg->jobid, runmsg->clientidx, -1, -EINVAL);
|
|
return -1;
|
|
}
|
|
|
|
if (!runmsg->taskhosts[0]->exdata
|
|
|| runmsg->taskhosts[0]->exdata[0] == '\0') {
|
|
task_ctx->startinfo->exdata = NULL;
|
|
XCECD_LOG(XCE_LOG_DEBUG3, "No exdata provided in runtasks request");
|
|
} else {
|
|
task_ctx->startinfo->exdata
|
|
= strdup((char*)runmsg->taskhosts[0]->exdata);
|
|
XCECD_LOG(XCE_LOG_DEBUG3, "Received exdata: %s",
|
|
task_ctx->startinfo->exdata);
|
|
|
|
if (!task_ctx->startinfo->exdata) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Failed to allocate memory for exdata");
|
|
free_startinfo(task_ctx->startinfo);
|
|
FREEUP(task_ctx);
|
|
xcecd_task_fail(runmsg->jobid, runmsg->clientidx, -1, -ENOMEM);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (!runmsg->taskhosts[0]->exechome
|
|
|| runmsg->taskhosts[0]->exechome[0] == '\0') {
|
|
task_ctx->startinfo->exechome = NULL;
|
|
XCECD_LOG(XCE_LOG_DEBUG3,
|
|
"No exechome provided in runtasks request");
|
|
} else {
|
|
task_ctx->startinfo->exechome
|
|
= strdup((char*)runmsg->taskhosts[0]->exechome);
|
|
if (!task_ctx->startinfo->exechome) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Failed to allocate memory for exechome");
|
|
free_startinfo(task_ctx->startinfo);
|
|
FREEUP(task_ctx);
|
|
xcecd_task_fail(runmsg->jobid, runmsg->clientidx, -1, -ENOMEM);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (!runmsg->taskhosts[0]->execcwd
|
|
|| runmsg->taskhosts[0]->execcwd[0] == '\0') {
|
|
task_ctx->startinfo->execcwd = NULL;
|
|
XCECD_LOG(XCE_LOG_DEBUG3,
|
|
"No execcwd provided in runtasks request");
|
|
} else {
|
|
task_ctx->startinfo->execcwd
|
|
= strdup((char*)runmsg->taskhosts[0]->execcwd);
|
|
if (!task_ctx->startinfo->execcwd) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Failed to allocate memory for execcwd");
|
|
free_startinfo(task_ctx->startinfo);
|
|
FREEUP(task_ctx);
|
|
xcecd_task_fail(runmsg->jobid, runmsg->clientidx, -1, -ENOMEM);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/* currently we do not consider pre/post exec for tasks */
|
|
#if 0
|
|
if (!runmsg->taskhosts[0]->q_pre_exec ||
|
|
runmsg->taskhosts[0]->q_pre_exec[0] == '\0') {
|
|
task_ctx->startinfo->q_pre_exec = NULL;
|
|
XCECD_LOG(XCE_LOG_DEBUG3,
|
|
"No queue level pre-exec provided in runtasks request");
|
|
} else {
|
|
task_ctx->startinfo->q_pre_exec = strdup((char*)runmsg->taskhosts[0]->q_pre_exec);
|
|
if (!task_ctx->startinfo->q_pre_exec) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Failed to allocate memory for queue level pre-exec command.");
|
|
free_startinfo(task_ctx->startinfo);
|
|
FREEUP(task_ctx);
|
|
xcecd_task_fail(runmsg->jobid,
|
|
runmsg->clientidx,
|
|
-1, -ENOMEM);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (!runmsg->taskhosts[0]->q_post_exec ||
|
|
runmsg->taskhosts[0]->q_post_exec[0] == '\0') {
|
|
task_ctx->startinfo->q_post_exec = NULL;
|
|
XCECD_LOG(XCE_LOG_DEBUG3,
|
|
"No queue level post-exec provided in runtasks request");
|
|
} else {
|
|
task_ctx->startinfo->q_post_exec = strdup((char*)runmsg->taskhosts[0]->q_post_exec);
|
|
if (!task_ctx->startinfo->q_post_exec) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Failed to allocate memory for queue level post-exec command.");
|
|
free_startinfo(task_ctx->startinfo);
|
|
FREEUP(task_ctx);
|
|
xcecd_task_fail(runmsg->jobid,
|
|
runmsg->clientidx,
|
|
-1, -ENOMEM);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (!runmsg->taskhosts[0]->job_pre_exec ||
|
|
runmsg->taskhosts[0]->job_pre_exec[0] == '\0') {
|
|
task_ctx->startinfo->job_pre_exec = NULL;
|
|
XCECD_LOG(XCE_LOG_DEBUG3,
|
|
"No job level pre-exec provided in runtasks request");
|
|
} else {
|
|
task_ctx->startinfo->job_pre_exec = strdup((char*)runmsg->taskhosts[0]->job_pre_exec);
|
|
if (!task_ctx->startinfo->job_pre_exec) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Failed to allocate memory for job level pre-exec command.");
|
|
free_startinfo(task_ctx->startinfo);
|
|
FREEUP(task_ctx);
|
|
xcecd_task_fail(runmsg->jobid,
|
|
runmsg->clientidx,
|
|
-1, -ENOMEM);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (!runmsg->taskhosts[0]->job_post_exec ||
|
|
runmsg->taskhosts[0]->job_post_exec[0] == '\0') {
|
|
task_ctx->startinfo->job_post_exec = NULL;
|
|
XCECD_LOG(XCE_LOG_DEBUG3,
|
|
"No job level post-exec provided in runtasks request");
|
|
} else {
|
|
task_ctx->startinfo->job_post_exec = strdup((char*)runmsg->taskhosts[0]->job_post_exec);
|
|
if (!task_ctx->startinfo->job_post_exec) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Failed to allocate memory for job level post-exec command.");
|
|
free_startinfo(task_ctx->startinfo);
|
|
FREEUP(task_ctx);
|
|
xcecd_task_fail(runmsg->jobid,
|
|
runmsg->clientidx,
|
|
-1, -ENOMEM);
|
|
return -1;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifndef DBUS_MANAGER
|
|
task_ctx->manager = (void*)cc_ctx->process_manager;
|
|
#endif
|
|
|
|
task_ctx->func = xcecd_process_command;
|
|
task_ctx->xce_jobid = runmsg->jobid;
|
|
task_ctx->clientidx = runmsg->clientidx;
|
|
|
|
if (submit_fork_task(cc_ctx->thread_pool, task_ctx) != 0) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to add task to thread pool");
|
|
free_startinfo(task_ctx->startinfo);
|
|
FREEUP(task_ctx);
|
|
xcecd_task_fail(runmsg->jobid, runmsg->clientidx, -1, -ENOMEM);
|
|
return -1;
|
|
} else {
|
|
XCECD_LOG(XCE_LOG_DEBUG, "Task added to thread pool queue");
|
|
}
|
|
|
|
} else {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to allocate memory for task context");
|
|
xcecd_task_fail(runmsg->jobid, runmsg->clientidx, -1, -ENOMEM);
|
|
return -1;
|
|
}
|
|
#endif // XRUN
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Handles the fetchfile request from the controller.
|
|
*
|
|
* Reads up to msg->max_bytes bytes from msg->filepath starting at msg->offset
|
|
* and sends the data back to the controller as an OPCode_reply.
|
|
*
|
|
* @param request Pointer to the xce_request_t containing the fetchfile msg.
|
|
* @return int 0 on success, -1 on failure.
|
|
*/
|
|
static int handle_fetchfile(xce_request_t* request)
|
|
{
|
|
xce_fetchfile_t* msg = request->body.fetchfilemsg;
|
|
xce_fetchfile_reply_t freply;
|
|
struct stat st;
|
|
uint8_t* buf = NULL;
|
|
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=%s path=%s offset=%lu max=%u",
|
|
jobidStr, msg->filepath, msg->offset, msg->max_bytes);
|
|
|
|
memset(&freply, 0, sizeof(freply));
|
|
freply.jobid = msg->jobid;
|
|
|
|
if (msg->filepath == NULL || msg->filepath[0] == '\0') {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Fetchfile: empty filepath");
|
|
freply.error = -EINVAL;
|
|
goto send;
|
|
}
|
|
|
|
/* Bound max_bytes to avoid huge allocations */
|
|
maxbytes = (msg->max_bytes == 0 || msg->max_bytes > 4 * 1024 * 1024)
|
|
? 4 * 1024 * 1024
|
|
: msg->max_bytes;
|
|
|
|
fd = open(msg->filepath, O_RDONLY);
|
|
if (fd < 0) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Fetchfile: open(%s) failed: %s",
|
|
msg->filepath, strerror(errno));
|
|
freply.error = -errno;
|
|
goto send;
|
|
}
|
|
|
|
if (fstat(fd, &st) < 0) {
|
|
freply.error = -errno;
|
|
close(fd);
|
|
goto send;
|
|
}
|
|
freply.file_size = (uint64_t)st.st_size;
|
|
|
|
buf = malloc(maxbytes);
|
|
if (buf == NULL) {
|
|
freply.error = -ENOMEM;
|
|
close(fd);
|
|
goto send;
|
|
}
|
|
|
|
nread = pread(fd, buf, maxbytes, (off_t)msg->offset);
|
|
close(fd);
|
|
|
|
if (nread < 0) {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Fetchfile: pread failed: %s",
|
|
strerror(errno));
|
|
freply.error = -errno;
|
|
freply.bytes_read = 0;
|
|
free(buf);
|
|
buf = NULL;
|
|
} else {
|
|
freply.error = 0;
|
|
freply.bytes_read = (uint32_t)nread;
|
|
freply.content = buf;
|
|
buf = NULL; /* ownership passed to freply */
|
|
}
|
|
|
|
send:
|
|
xcecli_reply_fetchfile(xcecli_ctx, request, &freply);
|
|
free(freply.content);
|
|
free(buf);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Handles the runtasks request from the controller.
|
|
*
|
|
* @param request Pointer to the xce_request_t structure containing the request
|
|
* data.
|
|
* @return int 0 on success, -1 on failure.
|
|
*/
|
|
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,
|
|
"Received job control request with invalid job ID");
|
|
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: %s",
|
|
jobidStr);
|
|
|
|
job_control_task_t* task_ctx = calloc(1, sizeof(job_control_task_t));
|
|
if (!task_ctx) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Failed to allocate memory for job control task context");
|
|
return -1;
|
|
}
|
|
|
|
task_ctx->jobid = d->jobid;
|
|
task_ctx->signal = d->u.siginfo->signo;
|
|
task_ctx->force = d->u.siginfo->force;
|
|
|
|
xcecd_worker_t* worker = find_worker_by_jobid(
|
|
(xcecd_worker_t*)*cc_ctx->worker_list, d->jobid);
|
|
|
|
if (worker) {
|
|
if (worker->job_status == JOB_PENDING
|
|
|| worker->job_status == JOB_STARTING) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Job is pending or starting, controller should not "
|
|
"pass the signal to %s.",
|
|
jobidStr);
|
|
|
|
// really should not happen, but if it does, we handle it
|
|
// gracefully
|
|
xcecd_job_status_error(d->jobid);
|
|
FREEUP(task_ctx);
|
|
return 0;
|
|
}
|
|
} else {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Failed to find worker for job ID: %s",
|
|
jobidStr);
|
|
FREEUP(task_ctx);
|
|
return -1;
|
|
}
|
|
|
|
#if !defined(VIRTUAL_CLUSTER)
|
|
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: %s",
|
|
jobidStr);
|
|
FREEUP(task_ctx);
|
|
return -1;
|
|
} else {
|
|
XCECD_LOG(XCE_LOG_DEBUG,
|
|
"Job control task submitted for job ID: %s", jobidStr);
|
|
}
|
|
FREEUP(task_ctx);
|
|
#else
|
|
// For virtual cluster, we handle job control tasks differently
|
|
XCECD_LOG(XCE_LOG_DEBUG,
|
|
"Virtual cluster mode, handling job control task directly");
|
|
FREEUP(task_ctx);
|
|
XCECD_LOG(XCE_LOG_DEBUG,
|
|
"Job control task handled directly for job ID: %s",
|
|
jobidStr);
|
|
|
|
worker->killed_signal = d->u.siginfo->signo; // Mark job as killed
|
|
#endif
|
|
} else if (d->action == JobControl_u_ckptoption) {
|
|
#if !defined(VIRTUAL_CLUSTER)
|
|
XCECD_LOG(XCE_LOG_DEBUG,
|
|
"Received job control request to set checkpoint options for "
|
|
"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: %s",
|
|
jobidStr);
|
|
return -1;
|
|
} else {
|
|
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 "
|
|
"cluster or xrun mode, which is not supported");
|
|
#endif
|
|
} else {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Received unknown job control action: %d for job ID: %s",
|
|
d->action, jobidStr);
|
|
return -1;
|
|
}
|
|
#endif // XRUN
|
|
return 0;
|
|
}
|
|
|
|
/* worker-rejoin-resync task 4.3 (Req 8.2/8.3 precedent): page-size cap for
|
|
* this worker's own OPCode_jobStateReport sends, matching the pktlimit
|
|
* value apiserver.c's own xce_push_data_req() call sites already use for
|
|
* the same {pos,count,n_total} pagination idiom. */
|
|
#define XCECD_RESYNC_REPORT_PKTLIMIT 100
|
|
|
|
/* Wall-clock (epoch) milliseconds -- same rationale and implementation as
|
|
* xcecd_worker_utils.c's private finished_job_wallclock_now_ms() (not
|
|
* reusable here: internal linkage, separate translation unit). Needed here
|
|
* because xcecd_resync.c's functions all take now_ms as a caller-captured
|
|
* parameter rather than sampling the clock themselves (task 4.1's own
|
|
* convention -- deterministic values in tests, real wall-clock in
|
|
* production), and this dispatch case is that production caller. */
|
|
static uint64_t resync_wallclock_now_ms(void)
|
|
{
|
|
struct timespec ts;
|
|
|
|
if (clock_gettime(CLOCK_REALTIME, &ts) != 0) {
|
|
return (uint64_t)time(NULL) * 1000ULL;
|
|
}
|
|
|
|
return (uint64_t)ts.tv_sec * 1000ULL + (uint64_t)ts.tv_nsec / 1000000ULL;
|
|
}
|
|
|
|
/**
|
|
* @brief Handles one incoming OPCode_jobStateQuery page from the controller
|
|
* (worker-rejoin-resync task 4.3, Requirements 3.8, 8.1, 8.3).
|
|
*
|
|
* Delegates to xcecd_resync_handle_query_page() (xcecd_resync.c), which
|
|
* reassembles a query that itself spans multiple incoming pages and, once
|
|
* the last page has arrived, answers it with a paginated
|
|
* OPCode_jobStateReport -- see that function's own header comment
|
|
* (xcecd_resync.h) for the resync_pending gate it also owns.
|
|
*
|
|
* @param request Pointer to the xce_request_t structure containing the
|
|
* request data.
|
|
* @return int 0 on success (whether or not this page completed the query),
|
|
* negative on error.
|
|
*/
|
|
static int handle_jobstatequery(xce_request_t* request)
|
|
{
|
|
endpoint_t* ep = xcecli_get_my_endpoint(xcecli_ctx);
|
|
xce_peerinfo_t* from = xcecli_get_myinfo(xcecli_ctx);
|
|
xcecd_worker_t* worker_list
|
|
= (cc_ctx != NULL) ? (xcecd_worker_t*)*cc_ctx->worker_list : NULL;
|
|
int rc;
|
|
|
|
if (ep == NULL || from == NULL) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"handle_jobstatequery: no active controller connection");
|
|
return -EINVAL;
|
|
}
|
|
|
|
rc = xcecd_resync_handle_query_page(
|
|
worker_list, ep, from, request->body.jobstatequerymsg,
|
|
resync_wallclock_now_ms(), XCECD_RESYNC_REPORT_PKTLIMIT);
|
|
if (rc < 0) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"handle_jobstatequery: failed to process query page "
|
|
"(rc=%d)",
|
|
rc);
|
|
return rc;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Handles an incoming OPCode_push config-push message from the
|
|
* controller (worker-rejoin-resync task 6.1, Requirement 3.7).
|
|
*
|
|
* Confirmed by inspection: before this task, myreq_handler's dispatch
|
|
* switch below had NO case for OPCode_push at all -- a config push
|
|
* (worker_push_config()/on_worker_join(), controller/worker.c) fell through
|
|
* to the `default: break;` case and was silently discarded in its
|
|
* entirety, even for the pre-existing clustername field.
|
|
* xcecd_finished_job_retention_set_window_ms()'s own doc comment
|
|
* (xcecd_worker_utils.h, task 4.1) independently confirms this same gap:
|
|
* "the worker daemon has no runtime delivery path for this ClusterConfig
|
|
* field today... this setter is the seam a later task wires the real
|
|
* operator-configured value into once that delivery path exists." This
|
|
* function is that delivery path, scoped narrowly to the ONE field this
|
|
* task needs (workerResyncWindowMs) -- clustername/confvars remain
|
|
* unconsumed by the worker, unchanged and out of this task's boundary; a
|
|
* general config-receive rework is not attempted here.
|
|
*
|
|
* Fire-and-forget, matching worker_push_config()'s own controller-side
|
|
* send (no reply is expected or sent back).
|
|
*
|
|
* @param request Pointer to the xce_request_t structure containing the
|
|
* request data.
|
|
* @return int 0 always -- a malformed/unexpected payload shape (wrong
|
|
* body_which, wrong ConfigData union arm, NULL conf4worker) is
|
|
* simply ignored, not an error to report back.
|
|
*/
|
|
static int handle_pushconfig(xce_request_t* request)
|
|
{
|
|
xce_configdata_t* configdata;
|
|
|
|
if (request->body_which != Request_body_configpushmsg)
|
|
return 0;
|
|
|
|
configdata = request->body.configpushmsg;
|
|
if (configdata == NULL
|
|
|| configdata->data_which != ConfigData_data_workerconfig
|
|
|| configdata->data.conf4worker == NULL) {
|
|
return 0;
|
|
}
|
|
|
|
xcecd_finished_job_retention_set_window_ms(
|
|
(uint64_t)configdata->data.conf4worker->worker_resync_window_ms);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Handles incoming requests from the controller.
|
|
*
|
|
* @param ctx Pointer to the I/O context structure.
|
|
* @return int 0 on success, -1 on failure.
|
|
*/
|
|
static int myreq_handler(xce_request_t* request, void* args)
|
|
{
|
|
int ret = 0;
|
|
|
|
XCECD_LOG(XCE_LOG_DEBUG, "Calling request handler...");
|
|
|
|
switch (request->opcode) {
|
|
case OPCode_startjob:
|
|
ret = handle_startjob(request);
|
|
break;
|
|
case OPCode_runtasks:
|
|
ret = handle_runtasks(request);
|
|
break;
|
|
case OPCode_jobcontrol:
|
|
ret = handle_jobcontrol(request);
|
|
break;
|
|
case OPCode_fetchfile:
|
|
ret = handle_fetchfile(request);
|
|
break;
|
|
case OPCode_jobStateQuery:
|
|
ret = handle_jobstatequery(request);
|
|
break;
|
|
case OPCode_push:
|
|
ret = handle_pushconfig(request);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* @brief Sends a start job reply to the controller.
|
|
*
|
|
* @param job Pointer to the job structure.
|
|
* @return bool true on success, false on failure.
|
|
*/
|
|
bool xcecd_reply_startjob(xcecd_job_t* job)
|
|
{
|
|
/* clang-format off */
|
|
//xce_ctx_t* ctx = cc_ctx->xce_ctx;
|
|
unsigned int pids[] = {job->job_pid};
|
|
job_pidinfo_t pidinfo = {
|
|
.jobid = job->job_id,
|
|
.npids = 1,
|
|
.pids = &pids[0],
|
|
.npgids = 0,
|
|
.pgids = NULL,
|
|
.exdata = NULL,
|
|
};
|
|
|
|
if (job->exdata[0] != '\0') {
|
|
pidinfo.exdata = strdup(job->exdata);
|
|
}
|
|
|
|
xcecli_notify_jobstart(xcecli_ctx, &pidinfo);
|
|
|
|
FREEUP(pidinfo.exdata);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Starts the controller connector by joining the cluster and setting up
|
|
* the endpoint handler.
|
|
*
|
|
* @return bool true on success, false on failure.
|
|
*/
|
|
bool controller_connector_start(void)
|
|
{
|
|
int rc;
|
|
int retry_limit = 0;
|
|
static int error_count = 0;
|
|
|
|
/* 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) {
|
|
XCECD_LOG(XCE_LOG_DEBUG,
|
|
"Failed to join cluster, retrying in %d seconds",
|
|
cc_ctx->join_interval);
|
|
error_count++;
|
|
if ((retry_limit > 0) && (error_count >= retry_limit)) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Failed to join cluster after %d retries",
|
|
retry_limit);
|
|
return false;
|
|
}
|
|
}
|
|
sleep(cc_ctx->join_interval);
|
|
} while (rc < 0 && cc_ctx->running);
|
|
|
|
// api_ctx_t* api_ctx = (api_ctx_t*)xcecli_ctx;
|
|
// cc_ctx->xce_ctx = &(api_ctx->ctx);
|
|
|
|
return true;
|
|
}
|
|
|
|
int xcecd_notify_job_fail(uint64_t jobid,
|
|
int stage,
|
|
int code)
|
|
{
|
|
job_failinfo_t failinfo = {
|
|
.jobid = jobid,
|
|
.stage = stage,
|
|
.code = code,
|
|
.detail = "no detail information"
|
|
};
|
|
|
|
XCECD_LOG(XCE_LOG_DEBUG, "Job failed and calling xcecli_notify_jobfail...");
|
|
return xcecli_notify_jobfail(xcecli_ctx, &failinfo);
|
|
}
|
|
|
|
/**
|
|
* @brief Marks a job as finished and sends the exit code to the controller.
|
|
*
|
|
* @param job Pointer to the job structure.
|
|
* @return int 0 on success, -1 on failure.
|
|
*/
|
|
int xcecd_job_finished(xcecd_job_t* job)
|
|
{
|
|
if (job->exit_code != 0) {
|
|
return xcecd_notify_job_fail(job->job_id,
|
|
job->stage,
|
|
job->exit_code);
|
|
}
|
|
|
|
xce_jobfinish_t myjob = {
|
|
.jobid = job->job_id,
|
|
.exit_code = 0
|
|
};
|
|
|
|
XCECD_LOG(XCE_LOG_DEBUG, "calling xcecli_jobfinish...");
|
|
return xcecli_notify_jobfinish(xcecli_ctx, &myjob);
|
|
}
|
|
|
|
/**
|
|
* @brief Marks a job as stopped and sends a notification to the controller.
|
|
*
|
|
* @param jobid The ID of the job to be marked as stopped.
|
|
* @return int 0 on success, -1 on failure.
|
|
*/
|
|
int xcecd_job_stopped(uint64_t jobid)
|
|
{
|
|
XCECD_LOG(XCE_LOG_DEBUG,
|
|
"Job stopped and calling xcecli_notify_jobstop...");
|
|
|
|
return xcecli_notify_jobstop(xcecli_ctx, jobid);
|
|
}
|
|
|
|
/**
|
|
* @brief Marks a job as resumed and sends a notification to the controller.
|
|
*
|
|
* @param jobid The ID of the job to be marked as resumed.
|
|
* @return int 0 on success, -1 on failure.
|
|
*/
|
|
int xcecd_job_resumed(uint64_t jobid)
|
|
{
|
|
XCECD_LOG(XCE_LOG_DEBUG,
|
|
"Job resumed and calling xcecli_notify_jobresume...");
|
|
|
|
return xcecli_notify_jobresume(xcecli_ctx, jobid);
|
|
}
|
|
|
|
/**
|
|
* @brief Reply job start error information to controller
|
|
* for some cases, we need to requeue the job
|
|
* and let controller to retry
|
|
* @return int 0 on success, -1 on failure.
|
|
*/
|
|
int xcecd_job_start_error(uint64_t jobid)
|
|
{
|
|
return xcecd_notify_job_fail(jobid, JOB_EXEC_STAGE_MAIN, -1);
|
|
}
|
|
|
|
/**
|
|
* @brief Reply job status error information to controller
|
|
* for some cases, we need to requeue the job
|
|
* and let controller to retry
|
|
* @return int 0 on success, -1 on failure.
|
|
*/
|
|
int xcecd_job_status_error(uint64_t jobid)
|
|
{
|
|
xce_jobfinish_t myjob = {
|
|
.jobid = jobid,
|
|
.exit_code = -1
|
|
};
|
|
|
|
XCECD_LOG(XCE_LOG_DEBUG, "Job status error and calling xcecli_jobfinish...");
|
|
return xcecli_jobfinish(xcecli_ctx, &myjob);
|
|
}
|
|
|
|
/**
|
|
* @brief Reply job checkpoint result to controller
|
|
* @return int 0 on success, -1 on failure
|
|
*/
|
|
int xcecd_job_ckpted(uint64_t jobid, int status, const char* info)
|
|
{
|
|
if (0 == status) {
|
|
XCECD_LOG(XCE_LOG_DEBUG, "Job checkpoint success and calling xcecli_notify_jobchkpt...");
|
|
} else {
|
|
XCECD_LOG(XCE_LOG_ERROR, "Job checkpoint failed and calling xcecli_notify_jobchkpt...");
|
|
}
|
|
|
|
return xcecli_notify_jobchkpt(xcecli_ctx, jobid, (char*)info);
|
|
}
|
|
|
|
/**
|
|
* @brief Starts a job on a remote host.
|
|
*
|
|
* @param hostname The name of the remote host.
|
|
* @param command The command to be executed on the remote host.
|
|
* @return int 0 on success, -1 on failure.
|
|
*/
|
|
int xcecd_job_remote_start(uint64_t jobid,
|
|
const char* hostname,
|
|
const char* command)
|
|
{
|
|
int rc = 0;
|
|
char jobidStr[64];
|
|
|
|
fmt_jobid(jobidStr, sizeof(jobidStr), jobid);
|
|
|
|
if (!cc_ctx || !cc_ctx->worker_list) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Controller connector context is not initialized");
|
|
return -1;
|
|
}
|
|
|
|
if (!hostname || !command) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Invalid parameters for remote job start");
|
|
return -1;
|
|
}
|
|
|
|
if (jobid <= 0) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Invalid job ID for remote job start");
|
|
return -1;
|
|
}
|
|
|
|
xcecd_worker_t* worker = find_worker_by_jobid((xcecd_worker_t *)*cc_ctx->worker_list,
|
|
jobid);
|
|
if (!worker) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Worker not found for job ID %s", jobidStr);
|
|
return -1;
|
|
}
|
|
|
|
xce_taskhost_t host[] = {
|
|
{.exechome = worker->exechome,
|
|
.execcwd = worker->execcwd,
|
|
.cmdline = strdup(command),
|
|
.exdata = worker->job_exdata,
|
|
.execuser = worker->execuser,
|
|
.launch_type = worker->launch_type,
|
|
.host = strdup(hostname),
|
|
.ntasks = 1},
|
|
{0}
|
|
};
|
|
|
|
xce_taskrun_t taskrun = {
|
|
.jobid = jobid,
|
|
.clientidx = -1,
|
|
.taskhosts = (xce_taskhost_t**)calloc(1, sizeof(xce_taskhost_t*)),
|
|
.nhosts = 1
|
|
};
|
|
|
|
taskrun.taskhosts[0] = &host[0];
|
|
|
|
rc = xcecli_runtask(xcecli_ctx, &taskrun);
|
|
if (rc < 0) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"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 %s will run on %s",
|
|
jobidStr, hostname);
|
|
return 0;
|
|
}
|
|
|
|
int xcecd_task_fail(uint64_t jobid, int clientidx,
|
|
int taskidx, int code)
|
|
{
|
|
xce_task_failinfo_t failinfo = {
|
|
.jobid = jobid,
|
|
.host = xce_client_cfg.from,
|
|
.clientidx = clientidx,
|
|
.taskidx = taskidx,
|
|
.code = code,
|
|
.detail = NULL
|
|
};
|
|
|
|
return xcecli_notify_taskfail(xcecli_ctx, &failinfo);
|
|
}
|
|
|
|
int xcecd_jobpost_done(uint64_t jobid)
|
|
{
|
|
XCECD_LOG(XCE_LOG_DEBUG,
|
|
"Job post done and calling xcecli_notify_jobpostdone...");
|
|
return xcecli_notify_jobpostdone(xcecli_ctx, 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 %s", jobidStr);
|
|
|
|
return xcecli_update_jusage(xcecli_ctx,
|
|
rusages->jobid,
|
|
rusages->npids,
|
|
rusages->pids,
|
|
rusages->npgids,
|
|
rusages->pgids,
|
|
rusages->count,
|
|
rusages->usages);
|
|
}
|
|
|
|
bool xcecd_send_load_indices(xce_resval_t** vals, int count) {
|
|
if (vals == NULL || count <= 0) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Invalid parameters for sending load indices");
|
|
return false;
|
|
}
|
|
|
|
if (xcecli_ctx == NULL) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"xcecli_ctx is not initialized");
|
|
return false;
|
|
}
|
|
|
|
int result = xcecli_update_resvals(xcecli_ctx, count, &vals[0]);
|
|
|
|
if (result != 0) {
|
|
XCECD_LOG(XCE_LOG_ERROR,
|
|
"Failed to send load indices to controller, error code: %d",
|
|
result);
|
|
return false;
|
|
}
|
|
|
|
XCECD_LOG(XCE_LOG_DEBUG,
|
|
"Successfully sent %d load indices to controller", count);
|
|
return true;
|
|
}
|