Implements the scheduler's first request-reply opcode (OPCode_schedAdmin). sched_handle_admin_request() (handler.c) decodes SchedAdminRequest and dispatches by action: recordStatus answers synchronously (pure read); recordStart/recordStop are deferred onto the existing pend_updates queue (XCE_SCHED_OP_ADMIN_RECORD) since sched_record_start()'s snapshot call requires the session_active==0 precondition only guaranteed on the driver thread, not the async request-handling thread. driver.c's xce_sched_driver_translate_pending_events() performs the deferred action and sends the reply via a new xcecli_reply_schedadmin() (client.c/client.h), correlated by seq copied before the original request is freed. Adversarial review found a genuine cross-thread race: sched_record_status() read driver-owned recording state (including a pointer the driver thread frees) from the vtask thread with no lock -- the codebase's first such race, missed by ASan (doesn't detect races). Fixed with a dedicated record_lock scoped strictly to in-memory field access, never spanning file I/O; sched_record_status() now strdup()s under the lock instead of returning a borrowed live pointer. Verified with a real two-thread lock-contention test (independently confirmed non-vacuous by removing the lock and observing the test fail) plus a deterministic use-after-free reproduction. 26/26 scheduler suite, clang-format clean.
156 lines
8.1 KiB
C
156 lines
8.1 KiB
C
#if !defined(_CLIENT_H_)
|
|
|
|
#define _CLIENT_H_ 1
|
|
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
|
|
#include "netio.h"
|
|
#include "request.h"
|
|
#include "xce/types.h"
|
|
#include "xceapi.h"
|
|
|
|
typedef void* xcecli_ctx_t;
|
|
|
|
/*
|
|
* xcecli_reconnect_cb: consumer rejoin hook invoked once per transparent
|
|
* reconnect/failover, AFTER the reconnect/recovery and BEFORE the interrupted
|
|
* op resumes. The callback CONTENT (join handshake, worker job re-sync) belongs
|
|
* to the consumer; libxce owns only the mechanism + ordering. Runs on the
|
|
* calling thread. Return 0 to proceed with the interrupted op; <0 to treat the
|
|
* rejoin as failed (the new primary rejected the join) so the failover loop
|
|
* rotates/backs off again instead of resuming. Pure clients register NO hook
|
|
* (NULL) — the loop then resumes directly.
|
|
*/
|
|
typedef int (*xcecli_reconnect_cb)(xcecli_ctx_t ctx, void* arg);
|
|
|
|
#define N_MAX_MASTERS (3)
|
|
|
|
typedef struct {
|
|
int nmasters;
|
|
char* masters[N_MAX_MASTERS];
|
|
int master_ports[N_MAX_MASTERS];
|
|
char* from;
|
|
char* key;
|
|
} xce_client_config_t;
|
|
|
|
#define xcecli_jobfinish(ctx, finish) xcecli_notify_jobfinish(ctx, finish)
|
|
|
|
extern int xcecli_ctx_init(xcecli_ctx_t*, xce_client_config_t*);
|
|
extern void xcecli_ctx_fini(xcecli_ctx_t);
|
|
extern int xcecli_set_handler(xcecli_ctx_t, xce_request_handler_t, void*);
|
|
extern int xcecli_set_reconnect_handler(xcecli_ctx_t, xcecli_reconnect_cb,
|
|
void*);
|
|
extern int xcecli_start(xcecli_ctx_t);
|
|
extern int xcecli_stop(xcecli_ctx_t);
|
|
extern int xcecli_wakeup(xcecli_ctx_t, int);
|
|
extern int xcecli_hello(xcecli_ctx_t, char*);
|
|
extern int xcecli_nop(xcecli_ctx_t);
|
|
extern int xcecli_heartbeat(xcecli_ctx_t, int);
|
|
extern int xcecli_shutdown(xcecli_ctx_t);
|
|
extern int xcecli_reslist(xcecli_ctx_t, xce_resource_t***);
|
|
extern int xcecli_join(xcecli_ctx_t, int, hostattrs_t*);
|
|
extern int xcecli_workerlist(xcecli_ctx_t, xce_worker_t***);
|
|
extern int xcecli_jobdispatch(xcecli_ctx_t, xce_job_dispatch_t*);
|
|
extern int xcecli_joblist(xcecli_ctx_t, xce_job_t***);
|
|
extern int xcecli_clusterconf(xcecli_ctx_t, xce_clusterconf_t**);
|
|
extern int xcecli_ctrlstatus(xcecli_ctx_t, xce_ctrlstatus_t**);
|
|
/*
|
|
* xcecli_ctrlstatus_primary (controller-status task 3.2): AUTHORITATIVE
|
|
* controller status read. Returns ONLY a reply whose responder role is the
|
|
* primary. When the first answer is from a non-primary, it follows the redirect
|
|
* (targets the reported primary via the reply's peer list, else rotates the
|
|
* configured controller list) until the primary answers or the failover budget
|
|
* is exhausted. On budget exhaustion / no reachable primary it returns a typed
|
|
* negative and NEVER hands back a non-primary hint reply (Req 9.1, 9.2). The
|
|
* any-node hint view is still available via xcecli_ctrlstatus above.
|
|
*/
|
|
extern int xcecli_ctrlstatus_primary(xcecli_ctx_t, xce_ctrlstatus_t**);
|
|
/*
|
|
* Pure decision helpers backing the authoritative read (no I/O — unit-testable
|
|
* and exposed for tests). xcecli_ctrlstatus_is_authoritative returns true iff
|
|
* the reply's responderRole is the primary role (ctrl_role_t PRIMARY == 0).
|
|
* xcecli_ctrlstatus_primary_addr maps the reply's primaryName to its host:port
|
|
* via the reply's peers list: returns 0 and fills host/port on a hit, -ENOENT
|
|
* when the primary is unknown/absent from peers, -EINVAL on a bad argument.
|
|
*/
|
|
extern bool xcecli_ctrlstatus_is_authoritative(const xce_ctrlstatus_t*);
|
|
extern int xcecli_ctrlstatus_primary_addr(const xce_ctrlstatus_t*,
|
|
char* host_out, size_t host_sz,
|
|
uint16_t* port_out);
|
|
extern int xcecli_notify_jobfinish(xcecli_ctx_t, xce_jobfinish_t*);
|
|
extern int xcecli_notify_jobstart(xcecli_ctx_t, job_pidinfo_t*);
|
|
extern int xcecli_notify_jobfail(xcecli_ctx_t c, job_failinfo_t*);
|
|
extern int xcecli_notify_jobstop(xcecli_ctx_t, uint64_t);
|
|
extern int xcecli_notify_jobresume(xcecli_ctx_t, uint64_t);
|
|
extern int xcecli_notify_jobpostdone(xcecli_ctx_t, uint64_t);
|
|
extern int xcecli_jobsubmit(xcecli_ctx_t, xce_job_submit_t*, uint64_t*);
|
|
extern int xcecli_update_resvals(xcecli_ctx_t, int, xce_resval_t**);
|
|
extern int xcecli_resval(xcecli_ctx_t, char*, char*, xce_resval_t***);
|
|
extern int xcecli_update_jusage(xcecli_ctx_t, uint64_t, int, int*, int, int*,
|
|
int, xce_rusage_t**);
|
|
extern int xcecli_notify(xcecli_ctx_t, xce_notify_data_t*);
|
|
extern int xcecli_jobctrl(xcecli_ctx_t, xce_jobctrl_t*);
|
|
extern int xcecli_notify_jobchkpt(xcecli_ctx_t, uint64_t, char*);
|
|
extern int xcecli_queuectrl(xcecli_ctx_t, xce_queuectrl_t*);
|
|
extern int xcecli_hostctrl(xcecli_ctx_t, xce_hostctrl_t*);
|
|
/* xcecli_upgradeconfig: submit a configuration upgrade (sha256 + serialised
|
|
* ClusterConfig blob) to the primary via OP_upgradeConfig, transparently
|
|
* surviving a primary change mid-call via XCECLI_FAILOVER_CALL (same
|
|
* write-op failover contract as xcecli_hostctrl/xcecli_queuectrl). Returns
|
|
* the server's reply status (0 on success). */
|
|
extern int xcecli_upgradeconfig(xcecli_ctx_t, const uint8_t *sha256,
|
|
size_t sha256_len, const uint8_t *bytes,
|
|
size_t bytes_len);
|
|
extern int xcecli_runtask(xcecli_ctx_t, xce_taskrun_t*);
|
|
extern int xcecli_notify_taskstart(xcecli_ctx_t, xce_task_pidinfo_t*);
|
|
extern int xcecli_notify_taskfail(xcecli_ctx_t, xce_task_failinfo_t*);
|
|
extern int xcecli_notify_taskfinish(xcecli_ctx_t, xce_task_finish_t*);
|
|
extern endpoint_t* xcecli_get_my_endpoint(xcecli_ctx_t);
|
|
extern xce_peerinfo_t* xcecli_get_myinfo(xcecli_ctx_t);
|
|
extern void xcecli_block_tasks(xcecli_ctx_t);
|
|
extern void xcecli_unblock_tasks(xcecli_ctx_t);
|
|
extern int xcecli_set_pend_reason(xcecli_ctx_t, xce_preason_set_t*);
|
|
extern int xcecli_clear_reason(xcecli_ctx_t, int, uint64_t*);
|
|
extern int xcecli_jobinfo(xcecli_ctx_t, uint64_t, xce_job_t**);
|
|
|
|
#define xcecli_free_ress(nres, ress) xce_free_ress(nres, ress)
|
|
#define xcecli_free_workers(nworkers, workers) \
|
|
xce_free_workers(nworkers, workers)
|
|
#define xcecli_free_jobs(njobs, jobs) xce_free_jobs(njobs, jobs)
|
|
#define xcecli_free_queues(nqueues, queues) xce_free_queues(nqueues, queues)
|
|
#define xcecli_free_groups(ngroups, groups) xce_free_groups(ngroups, groups)
|
|
#define xcecli_free_clusterconf(conf) xce_free_clusterconf(conf)
|
|
/* ctrlstatus reply is freed by the generated codec free. free_CtrlStatus()
|
|
* only frees the fields INSIDE the struct; the xce_ctrlstatus_t itself is
|
|
* calloc'd by decode_CtrlStatus_ptr() and must be freed via free_CtrlStatus_ptr()
|
|
* (which also nulls the caller's pointer) or every call leaks sizeof(xce_ctrlstatus_t)
|
|
* (bug-1401). */
|
|
#define xce_free_ctrlstatus(s) free_CtrlStatus_ptr(&(s))
|
|
#define xcecli_free_ctrlstatus(s) xce_free_ctrlstatus(s)
|
|
|
|
extern int xcecli_reply_fetchfile(xcecli_ctx_t, xce_request_t*, xce_fetchfile_reply_t*);
|
|
extern int xcecli_fetchfile(xcecli_ctx_t, xce_fetchfile_t*, xce_fetchfile_reply_t**);
|
|
|
|
/*
|
|
* xcecli_reply_schedadmin: send a SchedAdminReply back over `c`'s
|
|
* connection in response to a previously-received OPCode_schedAdmin
|
|
* request (scheduler-core spec, task 9.4, Req 5 AC 10). Mirrors
|
|
* xcecli_reply_fetchfile() above exactly, with one difference: it takes
|
|
* the original request's `seq` directly rather than an `xce_request_t*`,
|
|
* since `recordStart`/`recordStop` defer this reply to the driver thread
|
|
* (see driver.c's xce_sched_driver_translate_pending_events()), by which
|
|
* point the original xce_request_t has already been freed by the
|
|
* request-handling thread (xcectx.c's vtask_entry(), immediately after the
|
|
* registered handler returns) -- seq is the only field of it the reply
|
|
* needs (opcode is always OPCode_schedAdmin). `recordStatus` (answered
|
|
* synchronously from the handler thread, real request still in scope)
|
|
* uses this same function, simply passing `request->seq` directly.
|
|
* Returns xce_reply()'s own return value; -EINVAL if `c` or `areply` is
|
|
* NULL.
|
|
*/
|
|
extern int xcecli_reply_schedadmin(xcecli_ctx_t, uint64_t seq,
|
|
sched_admin_reply_t* areply);
|
|
|
|
#endif
|