Merge branch 'working' into job_peek_lchen

job_peek_lchen: Add fetchfile feature (job output/error peek proxy)
working: HA/Raft infrastructure (consensus, replication, config mgmt)

Conflict resolution summary:
- controller.capnp: fetchfile @41, fetchfilereply @18, fetchfilemsg @30
- cluster.c: bifurcated OPCode_reply for fetchfile + config-push ACK
- job.c: kept full fetchfile pending-request subsystem (~183 lines)
- request.c: kept both xce_fetchfile_req() and XCE_RETRY_DEFAULTS
- helper.c: added missing preVoteRequest/preVoteReply mappings
- All 15 new test registrations (test_cbuf.c + 14 HA tests)
This commit is contained in:
Lei Chen 2026-07-22 13:57:25 +08:00
commit 3a2094a461
529 changed files with 345174 additions and 5048 deletions

View file

@ -18,11 +18,11 @@ meson build && ninja -C build
Links against `libxce` from `src/lib/`.
## Conventions
- C standard: C99 or later
- C standard: gnu17
- Format with `clang-format`
- New scheduling policies → add as a plugin under `src/plugins/`, do not modify the core engine
- Shared types defined in `src/include/xce/` (libxce)
## See Also
- Shared library: `src/lib/CLAUDE.md`
- Root guidelines: `/workspace/CLAUDE.md`
- Root guidelines: `CLAUDE.md` (repo root)

View file

@ -21,6 +21,8 @@ typedef int (*plugin_job_state_change_hook_t)(void*, xce_sched_job_t*,
typedef int (*plugin_commit_decision_hook_t)(void*, xce_decision_t*);
typedef int (*plugin_job_election_hook_t)(void*, xce_sched_job_t*);
typedef int (*plugin_prepare_allocation_hook_t)(void*, xce_sched_ctx_t*);
typedef int (*plugin_job_rusage_hook_t)(void*, xce_sched_job_t*,
xce_job_rusages_t*);
/* initialize function of plugin */
typedef int (*plugin_init_fn_t)(void*);
@ -48,6 +50,7 @@ typedef struct {
plugin_commit_decision_hook_t commit_hook;
plugin_job_election_hook_t job_election_hook;
plugin_prepare_allocation_hook_t prepare_allocation_hook;
plugin_job_rusage_hook_t rusage_hook;
} plugin_hooks_t;
typedef struct {
@ -76,6 +79,7 @@ extern int plugin_end_session(void*);
extern int plugin_commit_decision(void*, xce_decision_t*);
extern int plugin_prepare_allocation(void*, xce_sched_ctx_t*);
extern int plugin_job_election(void*, xce_sched_job_t*);
extern int plugin_job_rusage_update(void*, xce_sched_job_t*, xce_job_rusages_t*);
extern int register_fcfs(void*);
extern int register_limit(void*);
extern int register_seqjob_allocator(void*);

View file

@ -93,6 +93,10 @@ typedef struct xce_sched_job_ {
uint32_t maxtasks;
xce_job_alloc_t* allocs;
xce_job_account_t* accounts;
/* Latest resource-usage report from the controller (Req 6 / bug-765 G-5).
* Owned by the job: replaced on each JOB_RUSAGE update, freed in
* kill_sched_job. NULL until the first rusage report. */
xce_job_rusages_t* rusages;
} xce_sched_job_t;
typedef struct {

View file

@ -15,6 +15,7 @@
#include "kvmgr.h"
#include "mylock.h"
#include "plugin.h"
#include "xce/cluster.h"
#include "xce/types.h"
#include "xlist.h"
#include "xvec.h"
@ -26,7 +27,16 @@ typedef enum {
XCE_SCHED_OP_UPDATE_CONFIG,
XCE_SCHED_OP_UPDATE_JOB,
XCE_SCHED_OP_JSTATE_CHANGE,
XCE_SCHED_OP_UPDATE_WORKER
XCE_SCHED_OP_UPDATE_WORKER,
XCE_SCHED_OP_UPDATE_QUEUE,
XCE_SCHED_OP_UPDATE_RESOURCES,
XCE_SCHED_OP_UPDATE_RESVALS_FULL,
XCE_SCHED_OP_UPDATE_RESVALS,
XCE_SCHED_OP_JOB_RUSAGE,
/* Notify-only single-job in-place field update. Distinct from
* XCE_SCHED_OP_UPDATE_JOB (the dataset bulk-create path) so a notify
* update to an existing job mutates it in place and never creates a job. */
XCE_SCHED_OP_UPDATE_JOB_FIELDS
} xce_sched_op_t;
typedef struct {
@ -38,6 +48,9 @@ typedef struct {
int njobs;
xce_job_t** jobs;
} jobupdate;
struct {
xce_job_t* job;
} jobfieldsupdate;
struct {
int nworkers;
xce_worker_t** workers;
@ -46,6 +59,23 @@ typedef struct {
uint64_t jobid;
uint32_t state;
} job_state_change;
struct {
xce_queue_t* queue;
} queueupdate;
struct {
int nresources;
xce_resource_t** resources;
} resourceupdate;
struct {
int nvals;
xce_resval_t** vals;
} resvalfullupdate;
struct {
xce_resval_list_t* resvals;
} resvalupdate;
struct {
xce_job_rusages_t* rusages;
} job_rusage;
} u;
} xce_sched_update_request_t;
@ -88,10 +118,12 @@ typedef struct {
_Atomic int eval_in_progress; /* 1 while eval_select loop runs */
_Atomic int staging_merge_complete; /* 1 after staging merge completes */
select_eval_ctx_t* sel_eval_ctx; /* session-level memoisation context; NULL between sessions */
xce_cluster_config_t cluster_cfg; /* HA candidate list — empty means single-host mode */
} xce_scheduler_t;
extern int sched_init(xce_scheduler_t*, xce_client_config_t*,
xce_request_handler_t);
extern void sched_set_cluster_cfg(xce_scheduler_t*, const xce_cluster_config_t*);
extern void sched_fini(xce_scheduler_t*);
extern int sched_init_config(xce_scheduler_t* sched, xce_conf4sched_t*);
extern int sched_update(xce_scheduler_t*);
@ -105,6 +137,9 @@ extern xce_sched_user_t* sched_get_user(xce_scheduler_t*, char*);
extern xce_sched_project_t* sched_get_project(xce_scheduler_t*, char*);
extern int sched_add_job(xce_scheduler_t*, xce_sched_job_t*);
extern int sched_add_queue(xce_scheduler_t*, xce_sched_queue_t*);
extern int sched_update_existing_queue(xce_scheduler_t*, xce_queue_t*);
extern int sched_update_existing_job(xce_scheduler_t*, xce_job_t*);
extern int sched_apply_job_rusage(xce_scheduler_t*, xce_job_rusages_t*);
extern int sched_add_user(xce_scheduler_t*, xce_sched_user_t*);
extern int sched_add_worker(xce_scheduler_t*, xce_sched_worker_t*);
extern int sched_add_project(xce_scheduler_t*, xce_sched_project_t*);

View file

@ -5,6 +5,7 @@
#include "controller.capnp.h"
#include "dataq.h"
#include "helper.h"
#include "xce/types.h"
#include "scheduler.h"
@ -18,21 +19,6 @@ static void enq_pend_update(xce_scheduler_t* sched,
mylock_unlock(&(sched->lock));
}
static int sched_handle_jobpush(xce_scheduler_t* sched, xce_jobs_data_t* jobs)
{
TRACE("%s...", __func__);
return 0;
}
static int sched_handle_workerpush(xce_scheduler_t* sched,
xce_workers_data_t* workers)
{
TRACE("%s...", __func__);
return 0;
}
static int sched_on_jobs(xce_scheduler_t* sched, int njobs, xce_job_t** jobs)
{
xce_sched_update_request_t* req;
@ -70,11 +56,57 @@ static int sched_on_workers(xce_scheduler_t* sched, int nworkers,
return 0;
}
static int sched_handle_resources(xce_scheduler_t* sched, int count,
xce_resource_t** resources)
{
xce_sched_update_request_t* req;
if (resources == NULL || count == 0) {
return 0;
}
req = (xce_sched_update_request_t*)calloc(1, sizeof(*req));
if (req == NULL) {
return -ENOMEM;
}
req->op = XCE_SCHED_OP_UPDATE_RESOURCES;
req->u.resourceupdate.nresources = count;
req->u.resourceupdate.resources = resources;
enq_pend_update(sched, req);
return 0;
}
static int sched_handle_resvals(xce_scheduler_t* sched, int count,
xce_resval_t** vals)
{
xce_sched_update_request_t* req;
if (vals == NULL || count == 0) {
return 0;
}
req = (xce_sched_update_request_t*)calloc(1, sizeof(*req));
if (req == NULL) {
return -ENOMEM;
}
req->op = XCE_SCHED_OP_UPDATE_RESVALS_FULL;
req->u.resvalfullupdate.nvals = count;
req->u.resvalfullupdate.vals = vals;
enq_pend_update(sched, req);
return 0;
}
static int sched_handle_dataset(xce_scheduler_t* sched, dataset_t* dataset)
{
int rc = 0;
TRACE("%s table = %d", __func__, dataset->table);
TRACE("%s table = %s", __func__, xce_dataset_table(dataset->table));
switch (dataset->table) {
case Data_data_schedjobs:
@ -96,8 +128,20 @@ static int sched_handle_dataset(xce_scheduler_t* sched, dataset_t* dataset)
}
break;
case Data_data_resources:
rc = sched_handle_resources(sched, dataset->nrows,
(xce_resource_t**)dataset->rows);
if (rc >= 0) {
dataset->nrows = 0;
dataset->rows = NULL;
}
break;
case Data_data_resvals:
rc = sched_handle_resvals(sched, dataset->nrows,
(xce_resval_t**)dataset->rows);
if (rc >= 0) {
dataset->nrows = 0;
dataset->rows = NULL;
}
break;
}
@ -126,10 +170,108 @@ static int sched_job_state_change(xce_scheduler_t* sched, uint64_t jobid,
return 0;
}
static int sched_handle_sched_update_job(xce_scheduler_t* sched,
xce_job_t* job)
{
xce_sched_update_request_t* req;
char jobidString[128];
if (job == NULL) {
return -EINVAL;
}
fmt_jobid(jobidString, sizeof(jobidString) - 1, job->jobid);
TRACE("%s %s updated", __func__, jobidString);
req = (xce_sched_update_request_t*)calloc(1, sizeof(*req));
if (req == NULL) {
return -ENOMEM;
}
/* Notify single-job update takes the in-place field-update path
* (XCE_SCHED_OP_UPDATE_JOB_FIELDS), NOT the dataset bulk-create path
* (XCE_SCHED_OP_UPDATE_JOB). An unknown jobid must yield -ENOENT, never a
* new job. The dataset bulk-create path (sched_on_jobs) is unchanged. */
req->op = XCE_SCHED_OP_UPDATE_JOB_FIELDS;
req->u.jobfieldsupdate.job = job;
enq_pend_update(sched, req);
return 0;
}
static int sched_handle_sched_update_queue(xce_scheduler_t* sched,
xce_queue_t* queue)
{
xce_sched_update_request_t* req;
if (queue == NULL) {
return -EINVAL;
}
req = (xce_sched_update_request_t*)calloc(1, sizeof(*req));
if (req == NULL) {
return -ENOMEM;
}
req->op = XCE_SCHED_OP_UPDATE_QUEUE;
req->u.queueupdate.queue = queue;
enq_pend_update(sched, req);
return 0;
}
static int sched_handle_sched_update_resvals(xce_scheduler_t* sched,
xce_resval_list_t* resvals)
{
xce_sched_update_request_t* req;
if (resvals == NULL || resvals->count == 0) {
return 0;
}
req = (xce_sched_update_request_t*)calloc(1, sizeof(*req));
if (req == NULL) {
return -ENOMEM;
}
req->op = XCE_SCHED_OP_UPDATE_RESVALS;
req->u.resvalupdate.resvals = resvals;
enq_pend_update(sched, req);
return 0;
}
static int sched_handle_sched_job_rusage(xce_scheduler_t* sched,
xce_job_rusages_t* rusages)
{
xce_sched_update_request_t* req;
if (rusages == NULL) {
return -EINVAL;
}
req = (xce_sched_update_request_t*)calloc(1, sizeof(*req));
if (req == NULL) {
return -ENOMEM;
}
req->op = XCE_SCHED_OP_JOB_RUSAGE;
req->u.job_rusage.rusages = rusages;
enq_pend_update(sched, req);
return 0;
}
int sched_handle_notify(xce_scheduler_t* sched, xce_notify_data_t* notify)
{
int rc = 0;
char jobidString[128];
int rc = 0;
TRACE("%s event = %s", __func__, xce_notify_event_name(notify->event));
switch (notify->event) {
case NotifyData_data_schedStartJob:
@ -145,15 +287,28 @@ int sched_handle_notify(xce_scheduler_t* sched, xce_notify_data_t* notify)
JOB_STAT_EXIT);
break;
case NotifyData_data_schedUpdateJob:
fmt_jobid(jobidString, sizeof(jobidString) - 1,
notify->data.sched_update_job->jobid);
TRACE("%s %s updated", __func__, jobidString);
rc = sched_handle_sched_update_job(sched,
notify->data.sched_update_job);
if (rc >= 0)
notify->data.sched_update_job = NULL;
break;
case NotifyData_data_schedUpdateQueue:
rc = sched_handle_sched_update_queue(sched,
notify->data.sched_update_queue);
if (rc >= 0)
notify->data.sched_update_queue = NULL;
break;
case NotifyData_data_schedUpdateResvals:
rc = sched_handle_sched_update_resvals(
sched, notify->data.sched_update_resvals);
if (rc >= 0)
notify->data.sched_update_resvals = NULL;
break;
case NotifyData_data_schedJobRusages:
rc = sched_handle_sched_job_rusage(sched,
notify->data.sched_job_rusages);
if (rc >= 0)
notify->data.sched_job_rusages = NULL;
break;
default:
rc = -EINVAL;
@ -162,11 +317,14 @@ int sched_handle_notify(xce_scheduler_t* sched, xce_notify_data_t* notify)
return rc;
}
int scheduler_req_handler(xce_request_t* request, void* args)
{
int rc = 0;
xce_scheduler_t* sched = (xce_scheduler_t*)args;
TRACE("%s op = %s", __func__, xce_request_op_name(request));
switch (request->opcode) {
case OPCode_push:
switch (request->body_which) {
@ -181,21 +339,6 @@ int scheduler_req_handler(xce_request_t* request, void* args)
request->body.configpushmsg->data.conf4sched = NULL;
}
break;
case Request_body_pushmsg:
/* data arriving */
switch (request->body.pushmsg->table) {
case Data_data_schedjobs:
/* job arriving */
rc = sched_handle_jobpush(sched,
request->body.pushmsg->data.jobs);
break;
case Data_data_schedworkers:
/* workers arriving */
rc = sched_handle_workerpush(
sched, request->body.pushmsg->data.workers);
break;
}
break;
case Request_body_datamsg:
rc = sched_handle_dataset(sched, request->body.datamsg);
break;

View file

@ -2,10 +2,12 @@
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "client.h"
#include "controller.capnp.h"
#include "xce/cluster.h"
#include "xce/types.h"
#include "scheduler.h"
@ -13,6 +15,7 @@
#include "resreq.h"
#include "utils.h"
extern int scheduler_req_handler(xce_request_t*, void*);
static char doc[] = "xce_scheduler";
@ -24,11 +27,15 @@ const char* argp_program_bug_address = BUG_EMAIL;
struct arguments {
char* logfile;
int verbose;
char* peers; /* --peers value (optional) */
char* ca_cert; /* --ca-cert value (optional) */
};
static struct argp_option options[] = {
{ "logfile", 'l', "FILE", 0, "The full path of log file" },
{ "verbose", 'v', 0, 0, "Produce verbose log" },
{ "logfile", 'l', "FILE", 0, "The full path of log file" },
{ "verbose", 'v', 0, 0, "Produce verbose log" },
{ "peers", 'P', "LIST", 0, "HA candidate list: [name=]host:port,..." },
{ "ca-cert", 'C', "FILE", 0, "CA certificate path for TLS verification" },
{ 0 }
};
@ -43,6 +50,12 @@ static error_t parse_opt(int key, char* arg, struct argp_state* state)
case 'v':
arguments->verbose++;
break;
case 'P':
arguments->peers = arg;
break;
case 'C':
arguments->ca_cert = arg;
break;
default:
return ARGP_ERR_UNKNOWN;
}
@ -109,6 +122,42 @@ int main(int argc, char* argv[])
mylog_init(logfile, loglevel, logsize, logcount);
/* P2.5-6: populate and validate xce_cluster_config_t when --peers given */
if (arguments.peers) {
xce_cluster_config_t cluster_cfg = {0};
if (xce_parse_peers(arguments.peers, &cluster_cfg) != 0) {
fprintf(stderr,
"xce_scheduler: --peers parse error: expected "
"\"[name=]host:port,...\" got \"%s\"\n",
arguments.peers);
return 1;
}
if (arguments.ca_cert) {
strncpy(cluster_cfg.ca_cert_path, arguments.ca_cert,
sizeof(cluster_cfg.ca_cert_path) - 1);
}
strncpy(cluster_cfg.identity, "xce_scheduler",
sizeof(cluster_cfg.identity) - 1);
if (xce_cluster_config_validate(&cluster_cfg) != 0) {
fprintf(stderr,
"xce_scheduler: invalid cluster config (missing "
"--ca-cert?).\n");
return 1;
}
if (cluster_cfg.candidates_count > 3) {
WARN("xce_scheduler: --peers has %d candidates; "
"only <=3 validated for HA",
cluster_cfg.candidates_count);
}
INFO("HA cluster config: %d candidate(s), identity=%s, ca_cert=%s",
cluster_cfg.candidates_count, cluster_cfg.identity,
cluster_cfg.ca_cert_path);
sched_set_cluster_cfg(&sched, &cluster_cfg);
} else {
INFO("No --peers flag; using single-controller path");
}
init_config(&config);
sched_init(&sched, &config, scheduler_req_handler);

View file

@ -358,6 +358,44 @@ int plugin_job_state_change(void* arg, xce_sched_job_t* job, uint32_t state)
return rc;
}
int plugin_job_rusage_update(void* arg, xce_sched_job_t* job,
xce_job_rusages_t* rusages)
{
xce_scheduler_t* sched = (xce_scheduler_t*)arg;
iterator_t it;
int rc;
if (sched == NULL) {
return -EINVAL;
}
rc = iter_make(&it, sched->plugins);
if (rc < 0) {
return rc;
}
for (;;) {
xce_sched_plugin_t* plugin = NULL;
rc = iter_next(&it, (void**)&plugin);
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
break;
}
if (plugin->hooks.rusage_hook != NULL) {
plugin->hooks.rusage_hook(plugin, job, rusages);
}
}
if (rc == RTTI_ITER_NO_DATA) {
rc = 0;
}
iter_destroy(&it);
return rc;
}
int plugin_job_election(void* arg, xce_sched_job_t* job)
{
xce_scheduler_t* sched = (xce_scheduler_t*)arg;

View file

@ -207,6 +207,7 @@ static void* fcfs_elect_job(xce_sched_plugin_t* plugin)
ordq_item_t* it = NULL;
int go = 0;
uint64_t jobid = 0;
char jobidString[64];
pthread_testcancel();
@ -233,7 +234,8 @@ static void* fcfs_elect_job(xce_sched_plugin_t* plugin)
if (sched_job_is_ready(j->job)) {
jobid = j->job->jobid;
TRACE("%s jobid %ld", __func__, jobid);
fmt_jobid(jobidString, sizeof(jobidString), jobid);
TRACE("%s jobid %s", __func__, jobidString);
cbuf_put(&(sched->jbuf), &jobid);
}

View file

@ -226,9 +226,11 @@ static int limit_job_election(void* arg, xce_sched_job_t* job)
xce_sched_obj_t* obj = job->schedobj;
int rc = 0;
iterator_t it;
xce_sched_ctx_t ctx = { 0 };
xce_sched_ctx_t ctx = { 0 };
char jobidString[64];
TRACE("%s job %lu plugin %s", __func__, job->jobid, plugin->name);
fmt_jobid(jobidString, sizeof(jobidString), job->jobid);
TRACE("%s job %s plugin %s", __func__, jobidString, plugin->name);
if (obj->configver >= sched->configver) {
return 0;

View file

@ -93,18 +93,20 @@ int put_resvals(zarray_t* array, int nres, int* idxs, float* vals)
return nres;
}
static int unsatisfy_(int nres, int* idxs, float* total, float* inuse,
float* uncomitted, float* wanted, int* unmeet_idxs,
float* unmeet_vals)
static int unsatisfy_(xce_scheduler_t* sched, int nres, int* idxs, float* total,
float* inuse, float* uncomitted, float* wanted,
int* unmeet_idxs, float* unmeet_vals)
{
int i, j;
int count;
count = 0;
for (i = 0; i < nres; i++) {
TRACE("%s resource %d total %f inuse %f uncomit %f wanted %f", __func__,
idxs[i], total[i], inuse[i], uncomitted[i], wanted[i]);
if (total[i] < inuse[i] + uncomitted[i] + wanted[i]) {
TRACE("%s resource %d total %f inuse %f uncomit %f wanted %f",
__func__, idxs[i], total[i], inuse[i], uncomitted[i],
wanted[i]);
count++;
}
}
@ -116,8 +118,15 @@ static int unsatisfy_(int nres, int* idxs, float* total, float* inuse,
j = 0;
for (i = 0; i < nres; i++) {
if (total[i] < inuse[i] + uncomitted[i] + wanted[i]) {
xce_resource_t* r = NULL;
float need = inuse[i] + uncomitted[i] + wanted[i] - total[i];
vget(sched->resmgr, idxs[i], (void**)&r);
TRACE("%s unmet resource %d (%s) shortfall %f", __func__, idxs[i],
r ? r->name : "?", need);
unmeet_idxs[j] = idxs[i];
unmeet_vals[j] = inuse[i] + wanted[i] + uncomitted[i] - total[i];
unmeet_vals[j] = need;
j++;
}
}
@ -125,14 +134,16 @@ static int unsatisfy_(int nres, int* idxs, float* total, float* inuse,
return count;
}
static host_eval_ctx_t*
build_host_eval_ctx(const xce_sched_worker_t* w, int num_res)
static host_eval_ctx_t* build_host_eval_ctx(const xce_sched_worker_t* w,
int num_res)
{
host_eval_ctx_t* ctx = calloc(1, sizeof(*ctx));
if (ctx == NULL) return NULL;
if (ctx == NULL)
return NULL;
if (num_res <= 0) {
/* valid empty context — numvals/strvals remain NULL, bounds checks protect access */
/* valid empty context — numvals/strvals remain NULL, bounds checks
* protect access */
ctx->num_res = 0;
return ctx;
}
@ -148,7 +159,8 @@ build_host_eval_ctx(const xce_sched_worker_t* w, int num_res)
for (int i = 0; i < num_res; i++) {
xce_val_t* v = (xce_val_t*)zarray_get((zarray_t*)&w->resvals, i);
if (v == NULL) continue;
if (v == NULL)
continue;
switch (v->which) {
case Value_u_fvalue:
numvals[i] = (double)v->u.fvalue;
@ -168,10 +180,10 @@ build_host_eval_ctx(const xce_sched_worker_t* w, int num_res)
return ctx;
}
static void
free_host_eval_ctx(host_eval_ctx_t* ctx)
static void free_host_eval_ctx(host_eval_ctx_t* ctx)
{
if (ctx == NULL) return;
if (ctx == NULL)
return;
free((void*)ctx->numvals);
free((void*)ctx->strvals);
free(ctx);
@ -179,7 +191,8 @@ free_host_eval_ctx(host_eval_ctx_t* ctx)
void sched_worker_release_host_ctx(xce_sched_worker_t* w)
{
if (w == NULL) return;
if (w == NULL)
return;
free_host_eval_ctx(w->host_eval_ctx);
w->host_eval_ctx = NULL;
}
@ -187,11 +200,12 @@ void sched_worker_release_host_ctx(xce_sched_worker_t* w)
int sched_meet_host_selection(xce_scheduler_t* sched, xce_sched_worker_t* w,
xce_select_t* s)
{
if (s == NULL || s->root == NULL) return 1;
if (s == NULL || s->root == NULL)
return 1;
/* Build host context lazily — reused for all jobs against this host. */
if (w->host_eval_ctx == NULL) {
int num_res = (int)w->resvals.caps;
int num_res = (int)w->resvals.caps;
w->host_eval_ctx = build_host_eval_ctx(w, num_res);
if (w->host_eval_ctx == NULL) {
/* OOM: conservatively pass host */
@ -209,7 +223,8 @@ int sched_meet_host_selection(xce_scheduler_t* sched, xce_sched_worker_t* w,
return (rc > 0) ? 1 : 0;
}
/* Fallback (no session context — e.g. unit tests or OOM at session start). */
/* Fallback (no session context — e.g. unit tests or OOM at session start).
*/
int rc = eval_select_simple(s->root, w->host_eval_ctx);
return (rc > 0) ? 1 : 0;
}
@ -249,7 +264,7 @@ static int sched_check_shared_satisfy_(xce_scheduler_t* sched, xvec_t* total,
return rc;
}
rc = unsatisfy_(nres, (int*)wanted->idxs.data, (float*)total->data,
rc = unsatisfy_(sched, nres, (int*)wanted->idxs.data, (float*)total->data,
(float*)inuse->data, (float*)uncomitted->data,
(float*)wanted->vals.data, (int*)unmeet->idxs.data,
(float*)unmeet->vals.data);
@ -324,7 +339,7 @@ static int sched_check_host_satisfy_(xce_scheduler_t* sched,
return rc;
}
rc = unsatisfy_(nres, (int*)wanted->idxs.data, (float*)total->data,
rc = unsatisfy_(sched, nres, (int*)wanted->idxs.data, (float*)total->data,
(float*)inuse->data, (float*)uncomitted->data,
(float*)wanted->vals.data, (int*)unmeet->idxs.data,
(float*)unmeet->vals.data);
@ -457,7 +472,7 @@ static int sched_try_alloc_on_host_(xce_sched_worker_t* w, va_list ap)
if (ctx->reason == NULL) {
return -errno;
}
add_job_preason(ctx->reason->reasons, PREASON_MATCH_FAIL, w->name,
NULL);
@ -489,18 +504,18 @@ static int sched_try_alloc_on_host_(xce_sched_worker_t* w, va_list ap)
int i;
if (ctx)
for (i = 0; i < ctx->unmeet.count; i++) {
xce_resource_t* r = NULL;
for (i = 0; i < ctx->unmeet.count; i++) {
xce_resource_t* r = NULL;
rc = vget(sched->resmgr, ((int*)ctx->unmeet.idxs.data)[i],
(void**)&r);
if (rc < 0) {
continue;
rc = vget(sched->resmgr, ((int*)ctx->unmeet.idxs.data)[i],
(void**)&r);
if (rc < 0) {
continue;
}
add_job_preason(ctx->reason->reasons, PREASON_NO_RESOURCE,
w->name, r->name);
}
add_job_preason(ctx->reason->reasons, PREASON_NO_RESOURCE, w->name,
r->name);
}
return 0;
}

View file

@ -16,9 +16,15 @@
void kill_sched_job(void* arg)
{
if (arg == NULL) {
xce_sched_job_t* job = (xce_sched_job_t*)arg;
if (job == NULL) {
return;
}
/* release the stored rusage report, if any (Req 6 / bug-765 G-5).
* free_JobRusages_ptr is NULL-safe and clears the field. */
free_JobRusages_ptr(&job->rusages);
}
request_res_t* make_resource_request(char* res, float value)
@ -278,11 +284,23 @@ xce_sched_worker_t* make_sched_worker(void* arg, xce_worker_t* w)
/* fill resource values */
for (i = 0; i < w->nresvals_to_sched; i++) {
int idx = w->resvals_to_sched[i]->idx;
xce_val_t* val = NULL;
xce_name_val_t* nv = w->resvals_to_sched[i];
xce_resource_t* r = NULL;
xce_val_t* val = NULL;
MOVE_PTR(val, w->resvals_to_sched[i]->val);
zarray_put(&(worker->resvals), idx, val);
if (nv == NULL || nv->name == NULL) {
continue;
}
if (key_find(sched->resmgr, (void*)nv->name, (void**)&r) < 0
|| r == NULL) {
WARN("%s unknown resource '%s' in worker resvals, skipping",
__func__, nv->name);
continue;
}
MOVE_PTR(val, nv->val);
zarray_put(&(worker->resvals), r->idx, val);
}
return worker;
@ -475,3 +493,141 @@ int sched_ctx_get_uncomit_host(xvec_t** host, xce_sched_ctx_t* ctx, int idx)
*host = v;
return 1;
}
int sched_update_existing_queue(xce_scheduler_t* sched, xce_queue_t* updated)
{
xce_sched_queue_t* q;
if (updated == NULL) {
return -EINVAL;
}
q = sched_get_queue(sched, updated->queue);
if (q == NULL) {
return -ENOENT;
}
q->status = updated->status;
q->priority = updated->priority;
q->accept_interval = updated->accept_interval;
q->sched_delay = updated->sched_delay;
if (updated->resreq != NULL) {
free(q->resreq);
q->resreq = makeString(updated->resreq);
}
plugin_queue_update(sched, q);
return 0;
}
/*
* sched_update_existing_job — apply a notify single-job update to an existing
* job IN PLACE. The job must already exist (created via the Data_data_schedjobs
* dataset path); this function never creates a job.
*
* - NULL payload → -EINVAL
* - unknown jobid → -ENOENT (no job created)
* - otherwise → update mutable fields (state, priority, queue,
* resource request) and notify plugins; return 0
*
* Runtime state (schedobj, allocs, accounts) is preserved — the live
* xce_sched_job_t object is mutated, not replaced. Mirrors the field-copy +
* resreq-replace pattern of sched_update_existing_queue().
*/
int sched_update_existing_job(xce_scheduler_t* sched, xce_job_t* updated)
{
xce_sched_job_t* job;
if (updated == NULL) {
return -EINVAL;
}
job = sched_get_job(sched, updated->jobid);
if (job == NULL) {
return -ENOENT;
}
/* state — always carried on xce_job_t */
job->state = updated->state;
/* mutable submit-derived fields */
if (updated->submit != NULL) {
/* queue — re-resolve the queue pointer by name. xce_job_t carries no
* per-job priority, so job priority follows the (possibly new) queue,
* consistent with the queue-derived priority model. */
if (updated->submit->queue != NULL) {
xce_sched_queue_t* q =
sched_get_queue(sched, updated->submit->queue);
if (q != NULL) {
job->queue = q;
job->priority = q->priority;
}
}
/* resource request — re-parse and swap; free the old parsed container.
* Mirrors sched_update_existing_queue's free-old/replace pattern. The
* inner select tree is intern/GC-owned, consistent with make_sched_job. */
if (updated->submit->resreq != NULL) {
xce_resreq_t* newreq =
(xce_resreq_t*)calloc(1, sizeof(*newreq));
if (newreq == NULL) {
return -ENOMEM;
}
char* errmsg = NULL;
int rc = parse_resreq(newreq, updated->submit->resreq,
sched->resmgr, &errmsg);
if (rc < 0) {
ERROR("%s parse_resreq failed: %s", __func__,
errmsg ? errmsg : "(null)");
FREEUP(errmsg);
free(newreq);
return -EINVAL;
}
free(job->resreq);
job->resreq = newreq;
}
}
plugin_job_update(sched, job);
return 0;
}
/*
* sched_apply_job_rusage — store a controller rusage report ON the matching
* job (Req 6, UB-6.1/6.2/6.3; bug-765 G-5). On success the job TAKES OWNERSHIP
* of `rusages` (the caller must not free it); the previous report, if any, is
* released. Returns:
* -EINVAL rusages is NULL
* -ENOENT no job with rusages->jobid (no state modified, ownership retained
* by the caller so it can free the payload)
* 0 stored on the job and plugins notified
*/
int sched_apply_job_rusage(xce_scheduler_t* sched, xce_job_rusages_t* rusages)
{
xce_sched_job_t* job;
if (rusages == NULL) {
return -EINVAL;
}
job = sched_get_job(sched, rusages->jobid);
if (job == NULL) {
return -ENOENT;
}
/* replace any prior report; the job now owns this one. free_JobRusages_ptr
* is NULL-safe and clears the field before we reassign. */
if (job->rusages != rusages) {
free_JobRusages_ptr(&job->rusages);
}
job->rusages = rusages;
plugin_job_rusage_update(sched, job, rusages);
return 0;
}

View file

@ -22,14 +22,12 @@
static int sched_session_active(xce_scheduler_t* sched)
{
return atomic_load_explicit(&sched->session_active,
memory_order_acquire);
return atomic_load_explicit(&sched->session_active, memory_order_acquire);
}
static int sched_eval_in_progress(xce_scheduler_t* sched)
{
return atomic_load_explicit(&sched->eval_in_progress,
memory_order_acquire);
return atomic_load_explicit(&sched->eval_in_progress, memory_order_acquire);
}
static int sched_staging_merge_complete(xce_scheduler_t* sched)
@ -228,6 +226,7 @@ static void* sched_allocator(void* arg)
xce_use(sched);
while (1) {
uint64_t jobid;
char jobidString[64];
xce_decision_t* decision = NULL;
xce_sched_job_t* job = NULL;
int rc = 0;
@ -244,7 +243,8 @@ static void* sched_allocator(void* arg)
job = sched_get_job(sched, jobid);
if (job == NULL) {
TRACE("%s job %lu can not be found", __func__, jobid);
fmt_jobid(jobidString, sizeof(jobidString), jobid);
TRACE("%s job %s can not be found", __func__, jobidString);
continue;
}
@ -259,9 +259,10 @@ static void* sched_allocator(void* arg)
/* job is pend */
xce_pendreason_t* reason = job->schedobj->reason;
WARN("job %ld is pending but pend reason is NULL", jobid);
if (reason != NULL) {
if (reason == NULL) {
fmt_jobid(jobidString, sizeof(jobidString), jobid);
WARN("job %s is pending but pend reason is NULL", jobidString);
} else {
reason->jobid = job->jobid;
decision = make_pend_decision(reason);
}
@ -302,6 +303,9 @@ static void sched_pend_update_killer(data_item_t* it)
free(req->u.jobupdate.jobs);
break;
case XCE_SCHED_OP_UPDATE_JOB_FIELDS:
free_Job_ptr(&(req->u.jobfieldsupdate.job));
break;
case XCE_SCHED_OP_UPDATE_WORKER:
for (i = 0; i < req->u.workerupdate.nworkers; i++) {
free_Worker_ptr(&(req->u.workerupdate.workers[i]));
@ -311,6 +315,34 @@ static void sched_pend_update_killer(data_item_t* it)
break;
case XCE_SCHED_OP_JSTATE_CHANGE:
break;
/* The five notify/dataset payloads below are owned by the request (the
* handler/dataset path nullifies its own pointer on enqueue). The dequeue
* copies values / makeResource constructs a fresh instance, so none of
* these are aliased into live state — free them here (bug-765 G-3). */
case XCE_SCHED_OP_UPDATE_QUEUE:
free_QueueConf_ptr(&(req->u.queueupdate.queue));
break;
case XCE_SCHED_OP_UPDATE_RESOURCES:
for (i = 0; i < req->u.resourceupdate.nresources; i++) {
free_Resource_ptr(&(req->u.resourceupdate.resources[i]));
}
free(req->u.resourceupdate.resources);
break;
case XCE_SCHED_OP_UPDATE_RESVALS_FULL:
for (i = 0; i < req->u.resvalfullupdate.nvals; i++) {
free_ResourceVal_ptr(&(req->u.resvalfullupdate.vals[i]));
}
free(req->u.resvalfullupdate.vals);
break;
case XCE_SCHED_OP_UPDATE_RESVALS:
free_ResValList_ptr(&(req->u.resvalupdate.resvals));
break;
case XCE_SCHED_OP_JOB_RUSAGE:
/* NULL after a successful sched_apply_job_rusage (job took ownership);
* non-NULL on the -ENOENT/-EINVAL path, freed here (bug-765 G-5).
* free_JobRusages_ptr is NULL-safe. */
free_JobRusages_ptr(&(req->u.job_rusage.rusages));
break;
default:
break;
}
@ -389,8 +421,8 @@ int sched_init(xce_scheduler_t* sched, xce_client_config_t* config,
sched->nextid = 1;
sched->configver = 0;
xstring_table_init(1024*1024);
xstring_table_init(1024 * 1024);
mylock_init(&(sched->lock));
cbuf_init(&(sched->wbuf), 1, sizeof(int));
cbuf_init(&(sched->jbuf), njobs, sizeof(uint64_t));
@ -430,14 +462,54 @@ int sched_init(xce_scheduler_t* sched, xce_client_config_t* config,
xcecli_set_handler(sched->apictx, handler, sched);
rc = xcecli_start(sched->apictx);
if (rc < 0) {
return rc;
}
/* HA mode: iterate candidates until one accepts as primary */
if (sched->cluster_cfg.candidates_count > 0) {
int joined = 0;
for (int i = 0; i < sched->cluster_cfg.candidates_count; i++) {
const char* host = sched->cluster_cfg.candidates[i].hostname;
int port = sched->cluster_cfg.candidates[i].port;
rc = xcecli_join(sched->apictx, HostRole_scheduler, NULL);
if (rc < 0) {
return rc;
config->nmasters = 1;
config->masters[0] = (char*)host;
config->master_ports[0] = port;
rc = xcecli_ctx_init(&(sched->apictx), config);
if (rc < 0) {
continue;
}
xcecli_set_handler(sched->apictx, handler, sched);
rc = xcecli_start(sched->apictx);
if (rc < 0) {
xcecli_ctx_fini(sched->apictx); /* free before next attempt */
continue;
}
rc = xcecli_join(sched->apictx, HostRole_scheduler, NULL);
if (rc == 0) {
joined = 1;
break;
}
/* Standby replied — tear down fully before next attempt */
xcecli_stop(sched->apictx);
xcecli_ctx_fini(sched->apictx);
}
if (!joined) {
return -1;
}
} else {
/* Single-host path (original) */
rc = xcecli_start(sched->apictx);
if (rc < 0) {
return rc;
}
rc = xcecli_join(sched->apictx, HostRole_scheduler, NULL);
if (rc < 0) {
return rc;
}
}
rc = pthread_create(&(sched->jobpicker), NULL, &sched_job_picker, sched);
@ -450,6 +522,15 @@ int sched_init(xce_scheduler_t* sched, xce_client_config_t* config,
return rc;
}
void sched_set_cluster_cfg(xce_scheduler_t* sched,
const xce_cluster_config_t* cfg)
{
if (sched == NULL || cfg == NULL) {
return;
}
sched->cluster_cfg = *cfg;
}
void sched_fini(xce_scheduler_t* sched)
{
void* res;
@ -520,11 +601,23 @@ static int sched_init_sharedInst(xce_scheduler_t* sched)
int i;
for (i = 0; i < sched->config->nresvals; i++) {
int idx = sched->config->resvals[i]->idx;
xce_val_t* val = NULL;
xce_name_val_t* nv = sched->config->resvals[i];
xce_resource_t* r = NULL;
xce_val_t* val = NULL;
MOVE_PTR(val, sched->config->resvals[i]->val);
zarray_put(&(sched->sharedInst), idx, val);
if (nv == NULL || nv->name == NULL) {
continue;
}
if (key_find(sched->resmgr, (void*)nv->name, (void**)&r) < 0
|| r == NULL) {
WARN("%s unknown resource '%s' in resvals, skipping",
__func__, nv->name);
continue;
}
MOVE_PTR(val, nv->val);
zarray_put(&(sched->sharedInst), r->idx, val);
}
return 0;
@ -614,6 +707,29 @@ static int sched_update_workers(xce_scheduler_t* sched, int nworkers,
return 0;
}
static char* sched_op_name(xce_sched_op_t op)
{
static char buf[32];
switch (op) {
case XCE_SCHED_OP_NONE: return "NONE";
case XCE_SCHED_OP_UPDATE_CONFIG: return "UPDATE_CONFIG";
case XCE_SCHED_OP_UPDATE_JOB: return "UPDATE_JOB";
case XCE_SCHED_OP_JSTATE_CHANGE: return "JSTATE_CHANGE";
case XCE_SCHED_OP_UPDATE_WORKER: return "UPDATE_WORKER";
case XCE_SCHED_OP_UPDATE_QUEUE: return "UPDATE_QUEUE";
case XCE_SCHED_OP_UPDATE_RESOURCES: return "UPDATE_RESOURCES";
case XCE_SCHED_OP_UPDATE_RESVALS_FULL: return "UPDATE_RESVALS_FULL";
case XCE_SCHED_OP_UPDATE_RESVALS: return "UPDATE_RESVALS";
case XCE_SCHED_OP_JOB_RUSAGE: return "JOB_RUSAGE";
case XCE_SCHED_OP_UPDATE_JOB_FIELDS: return "UPDATE_JOB_FIELDS";
default: break;
}
snprintf(buf, sizeof(buf), "UNKNOWN(%d)", (int)op);
return buf;
}
int sched_update(xce_scheduler_t* sched)
{
dataq_t* q = NULL;
@ -646,6 +762,9 @@ int sched_update(xce_scheduler_t* sched)
rc = sched_update_jobs(sched, req->u.jobupdate.njobs,
req->u.jobupdate.jobs);
break;
case XCE_SCHED_OP_UPDATE_JOB_FIELDS:
rc = sched_update_existing_job(sched, req->u.jobfieldsupdate.job);
break;
case XCE_SCHED_OP_UPDATE_WORKER:
rc = sched_update_workers(sched, req->u.workerupdate.nworkers,
req->u.workerupdate.workers);
@ -654,6 +773,85 @@ int sched_update(xce_scheduler_t* sched)
rc = sched_job_state_change(sched, req->u.job_state_change.jobid,
req->u.job_state_change.state);
break;
case XCE_SCHED_OP_UPDATE_QUEUE:
rc = sched_update_existing_queue(sched, req->u.queueupdate.queue);
break;
case XCE_SCHED_OP_UPDATE_RESOURCES: {
int i;
for (i = 0; i < req->u.resourceupdate.nresources; i++) {
xce_resource_t* r = req->u.resourceupdate.resources[i];
if (r == NULL) continue;
xce_resource_t* existing = makeResource(sched->resmgr, r);
if (existing == NULL) {
/* EEXIST: update-in-place on existing resource (add-or-update) */
xce_resource_t* found = NULL;
if (key_find(sched->resmgr, r->name, (void**)&found) == 0
&& found != NULL) {
/* Copy mutable integer fields; desc/name are identity,
* distributions/hbased_defined are deep-structs. */
found->valtype = r->valtype;
found->ordtype = r->ordtype;
found->flags = r->flags;
found->interval = r->interval;
WARN("%s updated existing resource %s", __func__, r->name);
} else {
WARN("%s makeResource failed for %s and lookup not found",
__func__, r->name);
}
}
}
rc = 0;
break;
}
case XCE_SCHED_OP_UPDATE_RESVALS_FULL: {
int i;
for (i = 0; i < req->u.resvalfullupdate.nvals; i++) {
xce_resval_t* rv = req->u.resvalfullupdate.vals[i];
xce_resource_t* r = NULL;
if (rv == NULL) {
continue;
}
if (key_find(sched->resmgr, rv->name, (void**)&r) < 0
|| r == NULL) {
WARN("%s unknown resource %s in resvals update", __func__,
rv->name);
continue;
}
zarray_put(&sched->sharedInst, r->idx, rv->val);
}
rc = 0;
break;
}
case XCE_SCHED_OP_UPDATE_RESVALS: {
int i;
xce_resval_list_t* resvals = req->u.resvalupdate.resvals;
for (i = 0; i < resvals->count; i++) {
xce_resval_t* rv = resvals->vals[i];
xce_sched_worker_t* w = NULL;
xce_resource_t* r = NULL;
if (rv == NULL) continue;
w = sched_get_worker(sched, rv->host);
if (w == NULL) continue;
if (key_find(sched->resmgr, rv->name, (void**)&r) < 0 || r == NULL) {
WARN("%s unknown resource %s in per-worker resvals update",
__func__, rv->name);
continue;
}
zarray_put(&w->resvals, r->idx, rv->val);
}
rc = 0;
break;
}
case XCE_SCHED_OP_JOB_RUSAGE:
/* Store the rusage report ON the job (Req 6 / bug-765 G-5). On
* success the job takes ownership; clear the request pointer so the
* killer does not free it. On failure (-ENOENT/-EINVAL) ownership
* stays with the request and the killer frees it. */
rc = sched_apply_job_rusage(sched, req->u.job_rusage.rusages);
if (rc == 0) {
req->u.job_rusage.rusages = NULL;
}
break;
default:
rc = -EINVAL;
break;
@ -662,7 +860,8 @@ int sched_update(xce_scheduler_t* sched)
sched_pend_update_killer(it);
if (rc < 0) {
/* something wrong? */
WARN("%s: op %s failed: %s (%d)", __func__,
sched_op_name(req->op), strerror(-rc), rc);
}
}
@ -775,7 +974,8 @@ static int sched_account_jobs(xce_scheduler_t* sched)
void sched_begin_session(xce_scheduler_t* sched)
{
atomic_store_explicit(&sched->session_active, 1, memory_order_release);
atomic_store_explicit(&sched->staging_merge_complete, 0, memory_order_release);
atomic_store_explicit(&sched->staging_merge_complete, 0,
memory_order_release);
/* Allocate a fresh memoisation context sized to the current interned-node
* population. sched_reset_workers (called below) frees each worker's
@ -791,7 +991,8 @@ void sched_begin_session(xce_scheduler_t* sched)
sched_account_jobs(sched);
/* Staging merge done — sched_end_session's GC guard reads this field. */
atomic_store_explicit(&sched->staging_merge_complete, 1, memory_order_release);
atomic_store_explicit(&sched->staging_merge_complete, 1,
memory_order_release);
return;
}
@ -807,8 +1008,7 @@ void sched_end_session(xce_scheduler_t* sched)
/* Guard: GC requires the evaluation pipeline to be idle */
if (sched_eval_in_progress(sched) || !sched_staging_merge_complete(sched)) {
TRACE("%s: GC skipped — eval_in_progress=%d staging_merge_complete=%d",
__func__,
sched_eval_in_progress(sched),
__func__, sched_eval_in_progress(sched),
sched_staging_merge_complete(sched));
return;
}
@ -830,8 +1030,8 @@ void sched_end_session(xce_scheduler_t* sched)
}
if (njobs >= cap) {
int newcap = cap * 2;
xce_sched_job_t** tmp = realloc(live_jobs,
newcap * sizeof(*live_jobs));
xce_sched_job_t** tmp
= realloc(live_jobs, newcap * sizeof(*live_jobs));
if (tmp == NULL) {
free(live_jobs);
live_jobs = NULL;
@ -851,7 +1051,8 @@ void sched_end_session(xce_scheduler_t* sched)
if (live_jobs == NULL) {
/* OOM: cannot safely GC — skip to avoid sweeping nodes still in use */
atomic_store_explicit(&sched->staging_merge_complete, 1, memory_order_release);
atomic_store_explicit(&sched->staging_merge_complete, 1,
memory_order_release);
return;
}
@ -903,7 +1104,8 @@ void sched_end_session(xce_scheduler_t* sched)
recompact_node_ids();
free(live_jobs);
atomic_store_explicit(&sched->staging_merge_complete, 1, memory_order_release);
atomic_store_explicit(&sched->staging_merge_complete, 1,
memory_order_release);
}
int sched_add_job(xce_scheduler_t* sched, xce_sched_job_t* j)

View file

@ -1,5 +1,5 @@
scheduler_test_src = [
'test.c'
'test.c',
]
scheduler_test_deps = [libxce_deps, librtti_dep, unity_dep, libxce_dep]
@ -11,3 +11,30 @@ scheduler_test = executable('sched_test',
include_directories:[incdir],
dependencies: [scheduler_test_deps],
install: false)
# White-box test for the notify single-job in-place update path
# (sched_update_existing_job). It #includes ../src/schedobj.c directly, so it
# is a standalone TU with its own main() — built as a separate executable.
sched_job_update_test = executable('sched_job_update_test',
['test_job_update.c'],
include_directories:[incdir],
dependencies: [scheduler_test_deps],
install: false)
# White-box test for the notify/dataset enqueue handlers. It #includes
# ../src/handler.c directly (enq_pend_update + the static handlers), so it is a
# standalone TU with its own main() — built as a separate executable.
sched_handlers_test = executable('sched_handlers_test',
['test_handlers.c'],
include_directories:[incdir],
dependencies: [scheduler_test_deps],
install: false)
# Register as meson tests so `meson test --suite scheduler` runs them all.
# detect_leaks=0 matches the suite convention (fixtures are not torn down).
test('scheduler test', scheduler_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])
test('scheduler job-update test', sched_job_update_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])
test('scheduler handlers test', sched_handlers_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])

View file

@ -5,6 +5,7 @@
#include <string.h>
#include <stdatomic.h>
#include <unity/unity.h>
#include "helper.h"
#include "xstring.h"
/* Pull resource.c into this TU so static helpers are visible for white-box testing.
@ -15,6 +16,22 @@
xce_sched_worker_t* sched_get_worker(xce_scheduler_t* s, char* n) { (void)s; (void)n; return NULL; }
int plugin_prepare_allocation(void* s, xce_sched_ctx_t* c) { (void)s; (void)c; return 0; }
int sched_ctx_get_uncomit_host(xvec_t** v, xce_sched_ctx_t* c, int i) { (void)v; (void)c; (void)i; return 0; }
int plugin_queue_update(void* s, xce_sched_queue_t* q) { (void)s; (void)q; return 0; }
xce_sched_queue_t* sched_get_queue(xce_scheduler_t* s, char* n) { (void)s; (void)n; return NULL; }
/* Stub: sched_update_existing_queue returns -EINVAL when updated is NULL,
* -ENOENT when queue not found (sched_get_queue stub returns NULL).
* Defined in schedobj.c; stubbed here for test linkage. */
int sched_update_existing_queue(xce_scheduler_t* sched, xce_queue_t* updated)
{
if (updated == NULL) {
return -EINVAL;
}
if (sched_get_queue(sched, updated->queue) == NULL) {
return -ENOENT;
}
return 0;
}
void setUp() { return; }
@ -163,6 +180,7 @@ test_meet_host_selection_always_true_passes(void)
int rc = sched_meet_host_selection(&sched, &w, &sel);
TEST_ASSERT_EQUAL_INT(1, rc);
sched_worker_release_host_ctx(&w);
zarray_fini(&w.resvals);
gc_clear_marks();
gc_sweep_select_nodes(NULL, 0);
@ -190,6 +208,7 @@ test_meet_host_selection_always_false_fails(void)
int rc = sched_meet_host_selection(&sched, &w, &sel);
TEST_ASSERT_EQUAL_INT(0, rc);
sched_worker_release_host_ctx(&w);
zarray_fini(&w.resvals);
gc_clear_marks();
gc_sweep_select_nodes(NULL, 0);
@ -305,6 +324,8 @@ test_meet_host_selection_numeric_match(void)
TEST_ASSERT_EQUAL_INT(0, sched_meet_host_selection(&sched, &w2, &sel));
sched_worker_release_host_ctx(&w8);
sched_worker_release_host_ctx(&w2);
zarray_fini(&w8.resvals);
zarray_fini(&w2.resvals);
gc_clear_marks();
@ -362,6 +383,8 @@ test_meet_host_selection_string_match(void)
TEST_ASSERT_EQUAL_INT(0, sched_meet_host_selection(&sched, &wcpu, &sel));
sched_worker_release_host_ctx(&wgpu);
sched_worker_release_host_ctx(&wcpu);
zarray_fini(&wgpu.resvals);
zarray_fini(&wcpu.resvals);
gc_clear_marks();
@ -371,6 +394,75 @@ test_meet_host_selection_string_match(void)
sel_optimize_test_reset();
}
/* ── Task 9.1: xce_notify_event_name ─────────────────────────────────── */
static void
test_notify_event_name_known_values(void)
{
TEST_ASSERT_EQUAL_STRING("schedStartJob",
xce_notify_event_name(NotifyData_data_schedStartJob));
TEST_ASSERT_EQUAL_STRING("schedExitJob",
xce_notify_event_name(NotifyData_data_schedExitJob));
TEST_ASSERT_EQUAL_STRING("schedUpdateJob",
xce_notify_event_name(NotifyData_data_schedUpdateJob));
TEST_ASSERT_EQUAL_STRING("schedUpdateQueue",
xce_notify_event_name(NotifyData_data_schedUpdateQueue));
TEST_ASSERT_EQUAL_STRING("schedUpdateResvals",
xce_notify_event_name(NotifyData_data_schedUpdateResvals));
TEST_ASSERT_EQUAL_STRING("schedJobRusages",
xce_notify_event_name(NotifyData_data_schedJobRusages));
}
static void
test_notify_event_name_unknown_value(void)
{
char* s = xce_notify_event_name(9999);
TEST_ASSERT_NOT_NULL(s);
/* must start with "UNKNOWN(" */
TEST_ASSERT_EQUAL_INT(0, strncmp(s, "UNKNOWN(", 8));
}
/* ── Task 9.1 (enum completeness): new xce_sched_op_t values compile ─── */
static void
test_new_sched_op_enum_values_exist(void)
{
/* Compile-time presence check — if any enum value is missing, this
* translation unit will fail to compile. */
xce_sched_op_t ops[] = {
XCE_SCHED_OP_UPDATE_QUEUE,
XCE_SCHED_OP_UPDATE_RESOURCES,
XCE_SCHED_OP_UPDATE_RESVALS_FULL,
XCE_SCHED_OP_UPDATE_RESVALS,
XCE_SCHED_OP_JOB_RUSAGE,
};
TEST_ASSERT_EQUAL_INT(5, (int)(sizeof(ops) / sizeof(ops[0])));
}
/* ── Task 9.5: sched_update_existing_queue NULL guard ───────────────── */
static void
test_sched_update_existing_queue_null_payload(void)
{
xce_scheduler_t sched;
memset(&sched, 0, sizeof(sched));
/* NULL updated → -EINVAL without touching scheduler state */
int rc = sched_update_existing_queue(&sched, NULL);
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
}
/* ── Task 9.8: dead symbols absent ──────────────────────────────────── */
static void
test_dead_pushmsg_symbols_absent(void)
{
/* If sched_handle_jobpush or sched_handle_workerpush were present, they
* would be included via ../src/handler.c — but handler.c cannot be
* included here (requires generated controller.capnp.h). The build-time
* link step is the definitive check; this test documents the intent. */
TEST_PASS();
}
#define TEST(x) \
do { \
if (check_test(&arguments, #x)) { \
@ -394,6 +486,11 @@ int main(int argc, char* argv[])
TEST(test_staging_merge_complete_gc_guard);
TEST(test_meet_host_selection_numeric_match);
TEST(test_meet_host_selection_string_match);
TEST(test_notify_event_name_known_values);
TEST(test_notify_event_name_unknown_value);
TEST(test_new_sched_op_enum_values_exist);
TEST(test_sched_update_existing_queue_null_payload);
TEST(test_dead_pushmsg_symbols_absent);
return UNITY_END();
}

View file

@ -0,0 +1,190 @@
/*
* test_handlers.c - white-box tests for the scheduler notify/dataset enqueue
* handlers (bug-765 G-6, tasks 9.2 + 9.3). Pulls the real (static) handlers in
* handler.c into this TU; enq_pend_update is local to handler.c, so we drive a
* real sched->pend_updates dataq and assert the correct op was enqueued (or
* skipped / -EINVAL) per each handler's contract.
*/
#define UNIT_TEST 1
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#include "dataq.h"
#include "mylock.h"
#include "scheduler.h"
/* Pull the real handlers under test into this TU. handler.c is self-contained
* for the enqueue path: enq_pend_update and sched_job_state_change are both
* static within it; the dispatchers call lib helpers (linked). The only
* scheduler-internal extern handler.c references (in the config-init request
* path, not exercised by these enqueue tests) is sched_init_config — stubbed. */
#include "../src/handler.c"
int sched_init_config(xce_scheduler_t* s, xce_conf4sched_t* c)
{
(void)s; (void)c; return 0;
}
void setUp(void) { return; }
void tearDown(void)
{
printf("\n-----------------------------------------------------\n");
return;
}
static void fixture_init(xce_scheduler_t* sched)
{
memset(sched, 0, sizeof(*sched));
mylock_init(&(sched->lock));
sched->pend_updates[0] = (dataq_t*)calloc(1, sizeof(dataq_t));
dataq_init(sched->pend_updates[0], NULL);
sched->pend_updates[1] = (dataq_t*)calloc(1, sizeof(dataq_t));
dataq_init(sched->pend_updates[1], NULL);
}
/* Dequeue one pending request (NULL if the queue is empty) and return its op
* via *op_out; frees the request shell (payloads are test-owned/leaked under
* the suite's leaks-off convention). Returns 1 if an item was present. */
static int pop_op(xce_scheduler_t* sched, xce_sched_op_t* op_out)
{
data_item_t* it = dataq_deq(sched->pend_updates[0], NULL);
xce_sched_update_request_t* req;
if (it == NULL) {
return 0;
}
req = ITPTR_TO_UPDATE_REQ(it);
if (op_out != NULL) {
*op_out = req->op;
}
free(req);
return 1;
}
/* ── 9.3: notify single-item handlers (NULL -> -EINVAL, valid -> enqueue) ── */
static void test_handle_update_job_enqueues_job_fields(void)
{
xce_scheduler_t sched;
xce_sched_op_t op = (xce_sched_op_t)-1;
fixture_init(&sched);
TEST_ASSERT_EQUAL_INT(-EINVAL, sched_handle_sched_update_job(&sched, NULL));
TEST_ASSERT_EQUAL_INT(0, pop_op(&sched, NULL)); /* NULL enqueued nothing */
xce_job_t* job = (xce_job_t*)calloc(1, sizeof(*job));
job->jobid = 42;
TEST_ASSERT_EQUAL_INT(0, sched_handle_sched_update_job(&sched, job));
TEST_ASSERT_EQUAL_INT(1, pop_op(&sched, &op));
/* 5.6: notify job update takes the in-place UPDATE_JOB_FIELDS path, NOT
* the dataset bulk-create UPDATE_JOB path. */
TEST_ASSERT_EQUAL_INT(XCE_SCHED_OP_UPDATE_JOB_FIELDS, op);
free(job);
}
static void test_handle_update_queue_enqueues_queue(void)
{
xce_scheduler_t sched;
xce_sched_op_t op = (xce_sched_op_t)-1;
fixture_init(&sched);
TEST_ASSERT_EQUAL_INT(-EINVAL,
sched_handle_sched_update_queue(&sched, NULL));
xce_queue_t* q = (xce_queue_t*)calloc(1, sizeof(*q));
TEST_ASSERT_EQUAL_INT(0, sched_handle_sched_update_queue(&sched, q));
TEST_ASSERT_EQUAL_INT(1, pop_op(&sched, &op));
TEST_ASSERT_EQUAL_INT(XCE_SCHED_OP_UPDATE_QUEUE, op);
free(q);
}
static void test_handle_job_rusage_enqueues_rusage(void)
{
xce_scheduler_t sched;
xce_sched_op_t op = (xce_sched_op_t)-1;
fixture_init(&sched);
TEST_ASSERT_EQUAL_INT(-EINVAL,
sched_handle_sched_job_rusage(&sched, NULL));
xce_job_rusages_t* ru = (xce_job_rusages_t*)calloc(1, sizeof(*ru));
ru->jobid = 7;
TEST_ASSERT_EQUAL_INT(0, sched_handle_sched_job_rusage(&sched, ru));
TEST_ASSERT_EQUAL_INT(1, pop_op(&sched, &op));
TEST_ASSERT_EQUAL_INT(XCE_SCHED_OP_JOB_RUSAGE, op);
free(ru);
}
static void test_handle_update_resvals_skips_empty_enqueues_nonempty(void)
{
xce_scheduler_t sched;
xce_sched_op_t op = (xce_sched_op_t)-1;
xce_resval_list_t empty = { .count = 0, .vals = NULL };
xce_resval_list_t full = { .count = 1, .vals = NULL };
fixture_init(&sched);
/* NULL or count==0 -> skip (return 0, nothing enqueued) */
TEST_ASSERT_EQUAL_INT(0, sched_handle_sched_update_resvals(&sched, NULL));
TEST_ASSERT_EQUAL_INT(0,
sched_handle_sched_update_resvals(&sched, &empty));
TEST_ASSERT_EQUAL_INT(0, pop_op(&sched, NULL));
/* count>0 -> enqueue UPDATE_RESVALS */
TEST_ASSERT_EQUAL_INT(0,
sched_handle_sched_update_resvals(&sched, &full));
TEST_ASSERT_EQUAL_INT(1, pop_op(&sched, &op));
TEST_ASSERT_EQUAL_INT(XCE_SCHED_OP_UPDATE_RESVALS, op);
}
/* ── 9.2: dataset handlers (skip on NULL/empty, enqueue on valid) ── */
static void test_handle_resources_skips_empty_enqueues_nonempty(void)
{
xce_scheduler_t sched;
xce_sched_op_t op = (xce_sched_op_t)-1;
xce_resource_t* one[1];
fixture_init(&sched);
TEST_ASSERT_EQUAL_INT(0, sched_handle_resources(&sched, 0, NULL));
TEST_ASSERT_EQUAL_INT(0, pop_op(&sched, NULL));
one[0] = (xce_resource_t*)calloc(1, sizeof(xce_resource_t));
TEST_ASSERT_EQUAL_INT(0, sched_handle_resources(&sched, 1, one));
TEST_ASSERT_EQUAL_INT(1, pop_op(&sched, &op));
TEST_ASSERT_EQUAL_INT(XCE_SCHED_OP_UPDATE_RESOURCES, op);
free(one[0]);
}
static void test_handle_resvals_full_skips_empty_enqueues_nonempty(void)
{
xce_scheduler_t sched;
xce_sched_op_t op = (xce_sched_op_t)-1;
xce_resval_t* one[1];
fixture_init(&sched);
TEST_ASSERT_EQUAL_INT(0, sched_handle_resvals(&sched, 0, NULL));
TEST_ASSERT_EQUAL_INT(0, pop_op(&sched, NULL));
one[0] = (xce_resval_t*)calloc(1, sizeof(xce_resval_t));
TEST_ASSERT_EQUAL_INT(0, sched_handle_resvals(&sched, 1, one));
TEST_ASSERT_EQUAL_INT(1, pop_op(&sched, &op));
TEST_ASSERT_EQUAL_INT(XCE_SCHED_OP_UPDATE_RESVALS_FULL, op);
free(one[0]);
}
int main(int argc, char* argv[])
{
(void)argc; (void)argv;
UNITY_BEGIN();
RUN_TEST(test_handle_update_job_enqueues_job_fields, 0);
RUN_TEST(test_handle_update_queue_enqueues_queue, 0);
RUN_TEST(test_handle_job_rusage_enqueues_rusage, 0);
RUN_TEST(test_handle_update_resvals_skips_empty_enqueues_nonempty, 0);
RUN_TEST(test_handle_resources_skips_empty_enqueues_nonempty, 0);
RUN_TEST(test_handle_resvals_full_skips_empty_enqueues_nonempty, 0);
return UNITY_END();
}

View file

@ -0,0 +1,364 @@
/*
* test_job_update.c - white-box tests for the notify single-job in-place
* update path (sched_update_existing_job), remediation of bug-765 / gap G-8+G-4.
*
* Strategy: pull the REAL sched_update_existing_job into this TU by including
* schedobj.c, then drive it against a REAL sched->jobs kvmgr seeded with a
* REAL xce_sched_job_t. The scheduler-internal externs schedobj.c references
* but which are NOT under test (and live in other .c files not linked here)
* are stubbed below. sched_get_job/sched_add_job semantics are exercised for
* real via the kvmgr key_find/key_add path.
*
* Asserts:
* - existing jobid : fields mutate IN PLACE (same object, runtime state kept)
* - unknown jobid : -ENOENT and NO job created
* - NULL payload : -EINVAL
*/
#define UNIT_TEST 1
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#include "kvmgr.h"
#include "scheduler.h"
#include "xstring.h"
/* Pull the real implementation under test into this TU. */
#include "../src/schedobj.c"
/* Stubs for scheduler-internal externs schedobj.c references but which are
* defined in other .c files not linked into this test. (make_sched_obj and
* kill_sched_job are static in schedobj.c and come in via the #include.) */
xce_sched_user_t* sched_get_user(xce_scheduler_t* s, char* n)
{
(void)s; (void)n; return NULL;
}
xce_sched_project_t* sched_get_project(xce_scheduler_t* s, char* n)
{
(void)s; (void)n; return NULL;
}
int plugin_job_update(void* s, xce_sched_job_t* j) { (void)s; (void)j; return 0; }
int plugin_queue_update(void* s, xce_sched_queue_t* q) { (void)s; (void)q; return 0; }
int plugin_job_rusage_update(void* s, xce_sched_job_t* j, xce_job_rusages_t* r)
{
(void)s; (void)j; (void)r; return 0;
}
xce_job_account_t* make_job_account(xce_job_alloc_t* a) { (void)a; return NULL; }
void free_job_account(xce_job_account_t* a) { (void)a; }
int sched_add_user(xce_scheduler_t* s, xce_sched_user_t* u) { (void)s; (void)u; return 0; }
int sched_add_project(xce_scheduler_t* s, xce_sched_project_t* p) { (void)s; (void)p; return 0; }
void sched_worker_release_host_ctx(xce_sched_worker_t* w) { (void)w; }
int plugin_prepare_allocation(void* s, xce_sched_ctx_t* c) { (void)s; (void)c; return 0; }
/* sched_get_queue: a tiny by-name lookup over a fixed test queue set. */
static xce_sched_queue_t* g_test_queues[8];
static int g_test_nqueues;
xce_sched_queue_t* sched_get_queue(xce_scheduler_t* s, char* name)
{
int i;
(void)s;
if (name == NULL) {
return NULL;
}
for (i = 0; i < g_test_nqueues; i++) {
if (g_test_queues[i] != NULL && g_test_queues[i]->name != NULL
&& strcmp(g_test_queues[i]->name, name) == 0) {
return g_test_queues[i];
}
}
return NULL;
}
/* REAL kvmgr-backed job add/get - same rtti path the production code uses. */
int sched_add_job(xce_scheduler_t* sched, xce_sched_job_t* j)
{
int rc;
lock(sched->jobs, RTTI_LOCK_WRITE);
rc = key_add(sched->jobs, (void*)j->jobid, j);
unlock(sched->jobs);
return rc;
}
xce_sched_job_t* sched_get_job(xce_scheduler_t* sched, uint64_t jobid)
{
int rc;
xce_sched_job_t* job = NULL;
lock(sched->jobs, RTTI_LOCK_READ);
rc = key_find(sched->jobs, (void*)jobid, (void**)&job);
unlock(sched->jobs);
if (rc <= 0) {
return NULL;
}
return job;
}
void setUp(void) { return; }
void tearDown(void)
{
printf("\n-----------------------------------------------------\n");
return;
}
/* Test fixture helpers */
static void fixture_init(xce_scheduler_t* sched)
{
kvmgr_params_t jobs_params = { .key_type = KVMGR_KEY_INT,
.nslots = 59,
.delta = 16,
.datakiller = kill_sched_job };
kvmgr_params_t res_params = { .key_type = KVMGR_KEY_STRING,
.nslots = 16,
.delta = 8,
.datakiller = NULL };
memset(sched, 0, sizeof(*sched));
resreq_init(1);
xstring_table_init(1024 * 16);
sched->jobs = makeInstance(&xkvmgr_type, &jobs_params);
sched->resmgr = makeInstance(&xkvmgr_type, &res_params);
g_test_nqueues = 0;
memset(g_test_queues, 0, sizeof(g_test_queues));
}
static xce_sched_queue_t* make_test_queue(const char* name, int32_t prio)
{
xce_sched_queue_t* q = (xce_sched_queue_t*)calloc(1, sizeof(*q));
q->name = makeString((char*)name);
q->priority = prio;
g_test_queues[g_test_nqueues++] = q;
return q;
}
/* A seeded existing job carrying live runtime state we must not lose. */
static xce_sched_job_t* seed_job(xce_scheduler_t* sched, uint64_t jobid)
{
xce_sched_job_t* job = (xce_sched_job_t*)calloc(1, sizeof(*job));
job->jobid = jobid;
job->state = JOB_STAT_PEND;
job->priority = 1;
job->resreq = (xce_resreq_t*)calloc(1, sizeof(*job->resreq));
/* schedobj is live runtime state that MUST survive an in-place update */
job->schedobj = (xce_sched_obj_t*)calloc(1, sizeof(*job->schedobj));
sched_add_job(sched, job);
return job;
}
static xce_job_t* make_update_payload(uint64_t jobid, unsigned int state,
const char* queue)
{
xce_job_t* j = (xce_job_t*)calloc(1, sizeof(*j));
j->jobid = jobid;
j->state = state;
j->submit = (job_submit_data_t*)calloc(1, sizeof(*j->submit));
if (queue != NULL) {
j->submit->queue = makeString((char*)queue);
}
return j;
}
/* Tests */
static void
test_update_existing_job_mutates_in_place(void)
{
xce_scheduler_t sched;
fixture_init(&sched);
make_test_queue("normal", 5);
xce_sched_job_t* job = seed_job(&sched, 42);
xce_sched_obj_t* schedobj = job->schedobj; /* capture live runtime ptr */
xce_job_t* upd = make_update_payload(42, JOB_STAT_RUN, "normal");
int rc = sched_update_existing_job(&sched, upd);
TEST_ASSERT_EQUAL_INT(0, rc);
/* SAME object mutated in place, not replaced */
TEST_ASSERT_EQUAL_PTR(job, sched_get_job(&sched, 42));
/* mutable fields updated */
TEST_ASSERT_EQUAL_UINT(JOB_STAT_RUN, job->state);
TEST_ASSERT_EQUAL_PTR(g_test_queues[0], job->queue);
TEST_ASSERT_EQUAL_INT(5, job->priority);
/* live runtime state preserved (object identity, not a fresh calloc) */
TEST_ASSERT_EQUAL_PTR(schedobj, job->schedobj);
/* upd->submit->queue is a makeString()-interned string owned by the
* xstring table — not caller-freeable; only free the calloc'd structs. */
free(upd->submit);
free(upd);
}
static void
test_update_unknown_job_returns_enoent_no_create(void)
{
xce_scheduler_t sched;
fixture_init(&sched);
/* No job seeded for jobid 999 */
xce_job_t* upd = make_update_payload(999, JOB_STAT_RUN, NULL);
int rc = sched_update_existing_job(&sched, upd);
TEST_ASSERT_EQUAL_INT(-ENOENT, rc);
/* And NO job was created via the update path */
TEST_ASSERT_NULL(sched_get_job(&sched, 999));
free(upd->submit);
free(upd);
}
static void
test_update_null_payload_returns_einval(void)
{
xce_scheduler_t sched;
fixture_init(&sched);
int rc = sched_update_existing_job(&sched, NULL);
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
}
/* Job rusage storage (Req 6 / bug-765 G-5) */
static xce_job_rusages_t* make_rusages(uint64_t jobid)
{
/* count==0 / usages==NULL keeps free_job_rusages a clean structural free. */
xce_job_rusages_t* r = (xce_job_rusages_t*)calloc(1, sizeof(*r));
r->jobid = jobid;
return r;
}
static void
test_rusage_stored_on_existing_job(void)
{
xce_scheduler_t sched;
fixture_init(&sched);
xce_sched_job_t* job = seed_job(&sched, 42);
xce_job_rusages_t* ru = make_rusages(42);
int rc = sched_apply_job_rusage(&sched, ru);
TEST_ASSERT_EQUAL_INT(0, rc);
/* stored ON the job (ownership transferred to the job) */
TEST_ASSERT_EQUAL_PTR(ru, job->rusages);
/* the job object itself is unchanged identity */
TEST_ASSERT_EQUAL_PTR(job, sched_get_job(&sched, 42));
/* job owns ru now; kill_sched_job/free_job_rusages will release it */
}
static void
test_rusage_replace_frees_prior(void)
{
xce_scheduler_t sched;
fixture_init(&sched);
xce_sched_job_t* job = seed_job(&sched, 7);
xce_job_rusages_t* ru1 = make_rusages(7);
xce_job_rusages_t* ru2 = make_rusages(7);
TEST_ASSERT_EQUAL_INT(0, sched_apply_job_rusage(&sched, ru1));
TEST_ASSERT_EQUAL_PTR(ru1, job->rusages);
/* second report replaces the first (prior freed, no leak/crash) */
TEST_ASSERT_EQUAL_INT(0, sched_apply_job_rusage(&sched, ru2));
TEST_ASSERT_EQUAL_PTR(ru2, job->rusages);
}
static void
test_rusage_unknown_job_returns_enoent(void)
{
xce_scheduler_t sched;
fixture_init(&sched);
xce_job_rusages_t* ru = make_rusages(999); /* no such job */
int rc = sched_apply_job_rusage(&sched, ru);
TEST_ASSERT_EQUAL_INT(-ENOENT, rc);
/* on -ENOENT the caller retains ownership */
free_JobRusages_ptr(&ru);
}
static void
test_rusage_null_returns_einval(void)
{
xce_scheduler_t sched;
fixture_init(&sched);
TEST_ASSERT_EQUAL_INT(-EINVAL, sched_apply_job_rusage(&sched, NULL));
}
/* Queue in-place update (Req 4 / task 9.5 — REAL, exercises the linked
* schedobj.c sched_update_existing_queue, not a local stub) */
static void
test_update_existing_queue_real(void)
{
xce_scheduler_t sched;
fixture_init(&sched);
xce_sched_queue_t* q = make_test_queue("q1", 1);
xce_queue_t upd;
memset(&upd, 0, sizeof(upd));
upd.queue = makeString("q1");
upd.status = 2;
upd.priority = 9;
upd.accept_interval = 5;
upd.sched_delay = 3;
upd.resreq = NULL; /* skip the resreq makeString branch */
int rc = sched_update_existing_queue(&sched, &upd);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_UINT(2, q->status);
TEST_ASSERT_EQUAL_INT(9, q->priority);
TEST_ASSERT_EQUAL_INT(5, q->accept_interval);
TEST_ASSERT_EQUAL_INT(3, q->sched_delay);
}
static void
test_update_unknown_queue_returns_enoent(void)
{
xce_scheduler_t sched;
fixture_init(&sched);
xce_queue_t upd;
memset(&upd, 0, sizeof(upd));
upd.queue = makeString("nope"); /* no such queue seeded */
TEST_ASSERT_EQUAL_INT(-ENOENT, sched_update_existing_queue(&sched, &upd));
}
static void
test_update_queue_null_returns_einval(void)
{
xce_scheduler_t sched;
fixture_init(&sched);
TEST_ASSERT_EQUAL_INT(-EINVAL, sched_update_existing_queue(&sched, NULL));
}
int main(int argc, char* argv[])
{
(void)argc; (void)argv;
UNITY_BEGIN();
RUN_TEST(test_update_existing_job_mutates_in_place, 0);
RUN_TEST(test_update_unknown_job_returns_enoent_no_create, 0);
RUN_TEST(test_update_null_payload_returns_einval, 0);
RUN_TEST(test_rusage_stored_on_existing_job, 0);
RUN_TEST(test_rusage_replace_frees_prior, 0);
RUN_TEST(test_rusage_unknown_job_returns_enoent, 0);
RUN_TEST(test_rusage_null_returns_einval, 0);
RUN_TEST(test_update_existing_queue_real, 0);
RUN_TEST(test_update_unknown_queue_returns_enoent, 0);
RUN_TEST(test_update_queue_null_returns_einval, 0);
return UNITY_END();
}