external-license-resources 3.3b: elim load-index collector in xcecd
Per-host user-script load indices (LSF elim model): a config-declared
script collector (external_resources: [{name, command, interval}]) in
xcecd registers getters into resource_sync's load-index registry, so
user-defined dynamic resources flow the existing resval chain
(collect -> xcecli_update_resvals -> controller -> scheduler) without
any new daemon.
- elim.c: libcyaml config parse (fail-loud at load), posix_spawn script
runner with kill-watchdog timeout (mirrors esd_script.c, no fork in
xcecd), first-line output parse (bare number or "<name> <value>"),
per-source interval cache (getter serves last value until next_run),
fail-open keep-last-value on script failure
- resource_sync.c: fix two init-order traps exposed by external
registration -- init_builtin_load_indices no longer re-callocs a
populated g_load_indices array (clobbered entries -> strcmp garbage
-> segfault), and register self-allocates when called before the
first collect (capacity-0 trap); new public
resource_sync_ensure_builtin_load_indices() so elim_register_sources
can register before lazy builtin init and builtin-name shadowing is
a clean -EEXIST at startup
- main.c: xcecd -c/--config option (elim config file); ownership
main.c -> xcecd_init -> resource_sync registration, with rollback on
init failure and cleanup in xcecd_cleanup
- tests: 18-case white-box elim_test (config/parse/getter-cache/
register/unregister/script-runner incl. timeout no-zombie), plus
stubs for the startup-order test
- t1/t2/t3 E2E: per-host dynres1 values visible in CLI (11/22/33 from
each node's own script); job with rusage[dynres1=15] RUNs on t2
(22 >= 15, t1=11 correctly skipped), RUN->DONE; over-ask
rusage[dynres1=50] stays PEND; second saturating job overflows to
t3; kill frees the reservation and a new job re-fills t2
Note: rusage clauses use square-bracket syntax rusage[...]; the
LSF-style paren form rusage(...) is parsed as a select expression and
rejected by the select grammar (observed live, pre-existing).
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
700f3d4e1a
commit
30c51ea147
11 changed files with 1471 additions and 7 deletions
83
src/worker/include/elim.h
Normal file
83
src/worker/include/elim.h
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
#ifndef ELIM_H
|
||||
#define ELIM_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/*
|
||||
* elim -- the LSF-elim-compatible external resource collector for xcecd
|
||||
* (external-license-resources design §3.3b). Each configured source names a
|
||||
* command run on this host (posix_spawn, watchdog kill at
|
||||
* elim_script_timeout_ms); the command prints "<name> <value>" (elim
|
||||
* format) or a bare number on stdout, and the value registers as a custom
|
||||
* load index on resource_sync's registry -- flowing the live per-host
|
||||
* report chain (collect_load_indices -> xcecd_send_load_indices ->
|
||||
* controller update_val_res -> scheduler RESVALS_DELTA) with zero
|
||||
* downstream changes.
|
||||
*
|
||||
* Per-host semantics by design: an elim script answers "how much does THIS
|
||||
* host have"; cluster-global pools (license servers) belong to xceesd.
|
||||
*/
|
||||
|
||||
typedef struct elim_source_s {
|
||||
char* name; /* resource name the script reports */
|
||||
char* command; /* shell command line (run via /bin/sh -c) */
|
||||
int interval; /* seconds between re-runs; 0 = every collect */
|
||||
} elim_source_t;
|
||||
|
||||
typedef struct elim_config_s {
|
||||
int nsrc;
|
||||
elim_source_t* src; /* array of nsrc sources */
|
||||
} elim_config_t;
|
||||
|
||||
/*
|
||||
* Parse the elim section of the worker config file:
|
||||
*
|
||||
* external_resources:
|
||||
* - name: dynres1
|
||||
* command: /opt/xce/scripts/report-dynres1.sh
|
||||
* interval: 30
|
||||
*
|
||||
* Returns 0, -ENOENT (file missing), -EINVAL (bad shape: a source without
|
||||
* name or command can never report -- fail loudly at load, not silently at
|
||||
* runtime), -ENOMEM.
|
||||
*/
|
||||
extern int elim_config_load(const char* path, elim_config_t* cfg_out);
|
||||
extern int elim_config_load_path(const char* dir, const char* name,
|
||||
elim_config_t* cfg_out);
|
||||
extern void elim_config_fini(elim_config_t* cfg);
|
||||
|
||||
/*
|
||||
* Register every configured source as a custom (non-builtin) load index on
|
||||
* resource_sync's registry. Returns 0, or -EEXIST if a source's name is
|
||||
* already taken (another source, or one of the 11 builtins -- a user script
|
||||
* must not shadow "mem"/"r15s"/...), or -EINVAL/-ENOMEM. Registration
|
||||
* happens at startup, before the sync thread's first collect -- the
|
||||
* registry self-initializes for that (the pre-phase "capacity reached (0)"
|
||||
* trap).
|
||||
*/
|
||||
extern int elim_register_sources(const elim_config_t* cfg);
|
||||
extern void elim_unregister_sources(const elim_config_t* cfg);
|
||||
|
||||
/*
|
||||
* Run one collection command and read its stdout. Returns 0 with `out`
|
||||
* NUL-terminated, -ETIMEDOUT when the watchdog killed the child,
|
||||
* -ENODATA on empty output, -EINVAL/-errno on spawn/read failure.
|
||||
* The child gets an empty signal mask and default dispositions; stderr is
|
||||
* /dev/null. Never blocks past elim_script_timeout_ms.
|
||||
*/
|
||||
extern int elim_script_collect(const char* command, char* out, size_t outsz);
|
||||
|
||||
/*
|
||||
* Parse an elim-format collection output: the first non-empty line carries
|
||||
* "<name> <value>" or a bare number; leading/trailing whitespace ignored.
|
||||
* Returns the value, or NAN on garbage/empty (caller owns the fail-open
|
||||
* policy).
|
||||
*/
|
||||
extern double elim_parse_script_output(const char* out);
|
||||
|
||||
/* Watchdog, milliseconds a collection command may run before it is killed
|
||||
* (overridable from tests / future CLI). */
|
||||
extern int elim_script_timeout_ms;
|
||||
|
||||
#endif // ELIM_H
|
||||
|
|
@ -19,4 +19,10 @@ bool resource_sync_register_load_index(const char* name, int valtype,
|
|||
bool is_builtin);
|
||||
bool resource_sync_unregister_load_index(const char* name);
|
||||
|
||||
/* Ensure the registry array exists and the 11 builtin load indices are in
|
||||
* it -- for external registrars (elim) that run before the sync thread's
|
||||
* first collect would lazily populate them, so their name-collision checks
|
||||
* see the builtins. */
|
||||
void resource_sync_ensure_builtin_load_indices(void);
|
||||
|
||||
#endif // RESOURCE_SYNC_H
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
#define XCECD_H
|
||||
|
||||
#include "controller_connector.h"
|
||||
#include "elim.h"
|
||||
#include "hardware_monitor.h"
|
||||
#include "job_information.h"
|
||||
#include "node_information.h"
|
||||
|
|
@ -63,6 +64,11 @@ typedef struct {
|
|||
* xcecd_start() already ran. candidates_count == 0 means no --peers was
|
||||
* given (single-host default). */
|
||||
xce_cluster_config_t cluster_cfg;
|
||||
/* elim sources from the -c config file's external_resources section
|
||||
* (design §3.3b). Loaded in main.c (elim_config_load); ownership moves
|
||||
* to xcecd_ctx by xcecd_init(), which registers the sources with
|
||||
* resource_sync at startup. */
|
||||
elim_config_t elim;
|
||||
} xcecd_config_t;
|
||||
|
||||
typedef struct {
|
||||
|
|
@ -88,6 +94,9 @@ typedef struct {
|
|||
char* log_dir;
|
||||
char* db_path;
|
||||
char* virtual_name;
|
||||
/* elim collectors (design §3.3b): sources loaded from the -c config
|
||||
* file, registered with resource_sync by xcecd_init(). */
|
||||
elim_config_t elim;
|
||||
/* Copied from xcecd_config_t by xcecd_init(); consumed by
|
||||
* controller_connector_init() (xcecd_start()) to populate
|
||||
* cc_ctx->cluster_cfg BEFORE controller_connector_start() fires the
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ xcecd_worker_srcs = [
|
|||
'src/xcecd.c',
|
||||
'src/connection_handler.c',
|
||||
'src/resource_sync.c',
|
||||
'src/elim.c',
|
||||
'src/node_information.c',
|
||||
'src/thread_pool.c',
|
||||
'src/hardware_monitor.c',
|
||||
|
|
@ -95,6 +96,7 @@ xcecd_srcs = [
|
|||
'src/connection_handler.c',
|
||||
'src/controller_connector.c',
|
||||
'src/resource_sync.c',
|
||||
'src/elim.c',
|
||||
'src/xcecd_worker_utils.c',
|
||||
'src/xcecd_resync.c',
|
||||
'src/hardware_monitor.c',
|
||||
|
|
|
|||
656
src/worker/src/elim.c
Normal file
656
src/worker/src/elim.c
Normal file
|
|
@ -0,0 +1,656 @@
|
|||
/* elim.c -- the LSF-elim-compatible external resource collector for xcecd
|
||||
* (external-license-resources design §3.3b).
|
||||
*
|
||||
* The worker already owns half the elim model as live code: resource_sync.c
|
||||
* keeps a load-index registry (11 builtins shaped after LSF's load index
|
||||
* name set) whose values flow the per-host report chain every 15 ticks --
|
||||
* collect_load_indices() -> xcecd_send_load_indices() -> controller
|
||||
* update_val_res (non-SHARED lands in w->host.resvals) -> scheduler
|
||||
* RESVALS_DELTA -> sched_check_host_satisfy_. What was missing is the
|
||||
* ENTRY: a mechanism for a user to register their own index. This module is
|
||||
* that entry -- config-driven script collectors registered as custom
|
||||
* (non-builtin) load indices at startup.
|
||||
*
|
||||
* posix_spawn(), not fork(): xcecd is multi-threaded (no-fork rule,
|
||||
* .kiro/steering/tech.md "Process Creation") and a forked child that
|
||||
* allocated or logged before exec could deadlock on a lock another thread
|
||||
* held. The spawn/watchdog shape mirrors esd_script.c (the xceesd
|
||||
* collector, same phase's global leg): pipe, poll(2) in 100ms slices,
|
||||
* kill(SIGKILL) at elim_script_timeout_ms, waitpid reaps -- a hung script
|
||||
* must not stall the sync thread.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include "elim.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <math.h>
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <spawn.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cyaml/cyaml.h>
|
||||
|
||||
#include "resource_sync.h"
|
||||
#include "xcelog.h"
|
||||
|
||||
/* Watchdog, milliseconds of wall clock a collection command may run before
|
||||
* it is killed (design §3.3b: 脚本超时看门狗 kill). */
|
||||
int elim_script_timeout_ms = 10 * 1000;
|
||||
|
||||
/* ── config parse (libcyaml, esd_config.c pattern) ────────────────────── */
|
||||
|
||||
typedef struct yaml_elim_source_s {
|
||||
char* name;
|
||||
char* command;
|
||||
long interval; /* 0 = every collect tick */
|
||||
} yaml_elim_source_t;
|
||||
|
||||
typedef struct yaml_elim_config_s {
|
||||
yaml_elim_source_t* external_resources;
|
||||
unsigned external_resources_count;
|
||||
} yaml_elim_config_t;
|
||||
|
||||
static const cyaml_schema_field_t elim_source_fields_schema[] = {
|
||||
CYAML_FIELD_STRING_PTR("name", CYAML_FLAG_POINTER, yaml_elim_source_t,
|
||||
name, 1, CYAML_UNLIMITED),
|
||||
CYAML_FIELD_STRING_PTR("command", CYAML_FLAG_POINTER, yaml_elim_source_t,
|
||||
command, 1, CYAML_UNLIMITED),
|
||||
CYAML_FIELD_INT("interval", CYAML_FLAG_OPTIONAL, yaml_elim_source_t,
|
||||
interval),
|
||||
CYAML_FIELD_END
|
||||
};
|
||||
|
||||
static const cyaml_schema_value_t elim_source_schema = {
|
||||
CYAML_VALUE_MAPPING(CYAML_FLAG_DEFAULT, yaml_elim_source_t,
|
||||
elim_source_fields_schema)
|
||||
};
|
||||
|
||||
static const cyaml_schema_field_t elim_config_fields_schema[] = {
|
||||
CYAML_FIELD_SEQUENCE("external_resources", CYAML_FLAG_POINTER,
|
||||
yaml_elim_config_t, external_resources,
|
||||
&elim_source_schema, 0, CYAML_UNLIMITED),
|
||||
CYAML_FIELD_END
|
||||
};
|
||||
|
||||
static const cyaml_schema_value_t elim_config_schema = {
|
||||
CYAML_VALUE_MAPPING(CYAML_FLAG_POINTER, yaml_elim_config_t,
|
||||
elim_config_fields_schema)
|
||||
};
|
||||
|
||||
int elim_config_load_path(const char* dir, const char* name,
|
||||
elim_config_t* cfg_out)
|
||||
{
|
||||
static cyaml_config_t cyaml_cfg = {
|
||||
.mem_fn = cyaml_mem,
|
||||
.log_level = CYAML_LOG_WARNING,
|
||||
.flags = CYAML_CFG_IGNORE_UNKNOWN_KEYS,
|
||||
};
|
||||
|
||||
yaml_elim_config_t* ycfg = NULL;
|
||||
cyaml_err_t err;
|
||||
char* path = NULL;
|
||||
elim_config_t cfg;
|
||||
int i;
|
||||
int rc;
|
||||
|
||||
if ((dir == NULL) || (name == NULL) || (cfg_out == NULL)) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
memset(cfg_out, 0, sizeof(*cfg_out));
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
|
||||
if (asprintf(&path, "%s/%s", dir, name) < 0) {
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
err = cyaml_load_file(path, &cyaml_cfg, &elim_config_schema,
|
||||
(void**)&ycfg, NULL);
|
||||
free(path);
|
||||
if (err != CYAML_OK) {
|
||||
/* Map file-not-found apart from bad-file so the startup log can
|
||||
* say which (esd_config.c's probe). */
|
||||
char probe[512];
|
||||
FILE* f;
|
||||
|
||||
snprintf(probe, sizeof(probe), "%s/%s", dir, name);
|
||||
f = fopen(probe, "r");
|
||||
if (f == NULL) {
|
||||
return -ENOENT;
|
||||
}
|
||||
fclose(f);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if (ycfg == NULL) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
cfg.nsrc = (int)ycfg->external_resources_count;
|
||||
cfg.src = NULL;
|
||||
|
||||
if (cfg.nsrc > 0) {
|
||||
cfg.src = (elim_source_t*)calloc((size_t)cfg.nsrc,
|
||||
sizeof(elim_source_t));
|
||||
if (cfg.src == NULL) {
|
||||
cyaml_free(&cyaml_cfg, &elim_config_schema, ycfg, 0);
|
||||
return -ENOMEM;
|
||||
}
|
||||
}
|
||||
|
||||
rc = 0;
|
||||
for (i = 0; i < cfg.nsrc; i++) {
|
||||
yaml_elim_source_t* ys = &(ycfg->external_resources[i]);
|
||||
|
||||
/* A source without name or command can never report -- fail
|
||||
* loudly at load, not silently at runtime. */
|
||||
if ((ys->name == NULL) || (ys->command == NULL)) {
|
||||
rc = -EINVAL;
|
||||
break;
|
||||
}
|
||||
|
||||
cfg.src[i].name = strdup(ys->name);
|
||||
cfg.src[i].command = strdup(ys->command);
|
||||
cfg.src[i].interval = (int)ys->interval;
|
||||
|
||||
if ((cfg.src[i].name == NULL) || (cfg.src[i].command == NULL)) {
|
||||
rc = -ENOMEM;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cyaml_free(&cyaml_cfg, &elim_config_schema, ycfg, 0);
|
||||
|
||||
if (rc < 0) {
|
||||
elim_config_fini(&cfg);
|
||||
return rc;
|
||||
}
|
||||
|
||||
*cfg_out = cfg;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int elim_config_load(const char* path, elim_config_t* cfg_out)
|
||||
{
|
||||
char* dir;
|
||||
char* name;
|
||||
char* slash;
|
||||
int rc;
|
||||
|
||||
if ((path == NULL) || (cfg_out == NULL)) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
dir = strdup(path);
|
||||
if (dir == NULL) {
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
slash = strrchr(dir, '/');
|
||||
if (slash == NULL) {
|
||||
name = dir;
|
||||
dir = strdup(".");
|
||||
if (dir == NULL) {
|
||||
free(name);
|
||||
return -ENOMEM;
|
||||
}
|
||||
} else {
|
||||
*slash = '\0';
|
||||
name = slash + 1;
|
||||
if (*name == '\0') {
|
||||
free(dir);
|
||||
return -EINVAL;
|
||||
}
|
||||
if (dir == name - 1) {
|
||||
/* path was "/name" -- dir is "" */
|
||||
free(dir);
|
||||
dir = strdup("/");
|
||||
if (dir == NULL) {
|
||||
return -ENOMEM;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rc = elim_config_load_path(dir, name, cfg_out);
|
||||
|
||||
free(dir);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
void elim_config_fini(elim_config_t* cfg)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (cfg == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < cfg->nsrc; i++) {
|
||||
free(cfg->src[i].name);
|
||||
free(cfg->src[i].command);
|
||||
}
|
||||
free(cfg->src);
|
||||
|
||||
memset(cfg, 0, sizeof(*cfg));
|
||||
}
|
||||
|
||||
/* ── output parse (elim contract) ─────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* The elim contract: the first non-empty line carries "<name> <value>" or
|
||||
* a bare number; leading/trailing whitespace ignored; anything else is
|
||||
* garbage -> NAN (the caller owns the fail-open policy). Same shape as
|
||||
* esd_parse_script_output -- a number is a number wherever it stands.
|
||||
*/
|
||||
double elim_parse_script_output(const char* out)
|
||||
{
|
||||
const char* p;
|
||||
char* endp = NULL;
|
||||
double v;
|
||||
|
||||
if (out == NULL) {
|
||||
return NAN;
|
||||
}
|
||||
|
||||
p = out;
|
||||
while (*p != '\0') {
|
||||
while ((*p == ' ') || (*p == '\t') || (*p == '\r') || (*p == '\n')) {
|
||||
p++;
|
||||
}
|
||||
if (*p == '\0') {
|
||||
break;
|
||||
}
|
||||
|
||||
v = strtod(p, &endp);
|
||||
if ((endp != NULL) && (endp != p)) {
|
||||
return v;
|
||||
}
|
||||
|
||||
/* skip this non-numeric token and try the next one */
|
||||
while ((*p != '\0') && (*p != ' ') && (*p != '\t') && (*p != '\r')
|
||||
&& (*p != '\n')) {
|
||||
p++;
|
||||
}
|
||||
}
|
||||
|
||||
return NAN;
|
||||
}
|
||||
|
||||
/* ── script runner (posix_spawn + watchdog, esd_script.c shape) ───────── */
|
||||
|
||||
static int elim_script_spawn(const char* command, int out_fd, pid_t* pid_out)
|
||||
{
|
||||
posix_spawn_file_actions_t fa;
|
||||
posix_spawnattr_t attr;
|
||||
sigset_t none, all;
|
||||
char* argv[] = { (char*)"sh", (char*)"-c", (char*)command, NULL };
|
||||
extern char** environ;
|
||||
pid_t pid;
|
||||
int rc;
|
||||
|
||||
sigemptyset(&none);
|
||||
sigfillset(&all);
|
||||
|
||||
rc = posix_spawn_file_actions_init(&fa);
|
||||
if (rc == 0) {
|
||||
rc = posix_spawn_file_actions_adddup2(&fa, out_fd, STDOUT_FILENO);
|
||||
}
|
||||
if (rc == 0) {
|
||||
rc = posix_spawn_file_actions_addopen(&fa, STDERR_FILENO, "/dev/null",
|
||||
O_WRONLY, 0);
|
||||
}
|
||||
if (rc == 0) {
|
||||
rc = posix_spawnattr_init(&attr);
|
||||
}
|
||||
if (rc == 0) {
|
||||
rc = posix_spawnattr_setflags(
|
||||
&attr, POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF);
|
||||
}
|
||||
if (rc == 0) {
|
||||
rc = posix_spawnattr_setsigmask(&attr, &none);
|
||||
}
|
||||
if (rc == 0) {
|
||||
rc = posix_spawnattr_setsigdefault(&attr, &all);
|
||||
}
|
||||
if (rc == 0) {
|
||||
rc = posix_spawn(&pid, "/bin/sh", &fa, &attr, argv, environ);
|
||||
}
|
||||
posix_spawnattr_destroy(&attr);
|
||||
posix_spawn_file_actions_destroy(&fa);
|
||||
|
||||
if (rc != 0) {
|
||||
return -rc;
|
||||
}
|
||||
|
||||
*pid_out = pid;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int elim_script_collect(const char* command, char* out, size_t outsz)
|
||||
{
|
||||
int fds[2];
|
||||
pid_t pid = -1;
|
||||
size_t total = 0;
|
||||
int elapsed_ms = 0;
|
||||
int rc;
|
||||
int wstatus = 0;
|
||||
struct pollfd pfd;
|
||||
|
||||
if ((command == NULL) || (*command == '\0') || (out == NULL)
|
||||
|| (outsz == 0)) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
out[0] = '\0';
|
||||
|
||||
if (pipe(fds) != 0) {
|
||||
rc = -errno;
|
||||
XCECD_LOG(XCE_LOG_ERROR, "elim: pipe failed (%s)", strerror(-rc));
|
||||
return rc;
|
||||
}
|
||||
|
||||
rc = elim_script_spawn(command, fds[1], &pid);
|
||||
close(fds[1]); /* parent's copy -- child holds the write end now */
|
||||
if (rc < 0) {
|
||||
close(fds[0]);
|
||||
XCECD_LOG(XCE_LOG_ERROR, "elim: spawn failed for \"%s\" (%s)", command,
|
||||
strerror(-rc));
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Bound the read at the watchdog deadline: a script that hangs must
|
||||
* not stall the sync thread -- the value it was going to report
|
||||
* belongs to a dead world anyway. */
|
||||
pfd.fd = fds[0];
|
||||
pfd.events = POLLIN;
|
||||
|
||||
while (total < outsz - 1) {
|
||||
int pret = poll(&pfd, 1, 100);
|
||||
|
||||
if (pret < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
rc = -errno;
|
||||
XCECD_LOG(XCE_LOG_ERROR, "elim: poll failed for \"%s\" (%s)",
|
||||
command, strerror(-rc));
|
||||
kill(pid, SIGKILL);
|
||||
waitpid(pid, &wstatus, 0);
|
||||
close(fds[0]);
|
||||
return rc;
|
||||
}
|
||||
if (pret == 0) {
|
||||
elapsed_ms += 100;
|
||||
if (elapsed_ms >= elim_script_timeout_ms) {
|
||||
XCECD_LOG(XCE_LOG_WARN,
|
||||
"elim: command \"%s\" exceeded %d ms -- killing "
|
||||
"it, value not refreshed this round",
|
||||
command, elim_script_timeout_ms);
|
||||
kill(pid, SIGKILL);
|
||||
waitpid(pid, &wstatus, 0);
|
||||
close(fds[0]);
|
||||
return -ETIMEDOUT;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pfd.revents & (POLLERR | POLLHUP)) {
|
||||
ssize_t n = read(fds[0], out + total, outsz - 1 - total);
|
||||
if (n > 0) {
|
||||
total += (size_t)n;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (pfd.revents & POLLIN) {
|
||||
ssize_t n = read(fds[0], out + total, outsz - 1 - total);
|
||||
if (n <= 0) {
|
||||
break; /* EOF or error: done reading */
|
||||
}
|
||||
total += (size_t)n;
|
||||
}
|
||||
}
|
||||
|
||||
out[total] = '\0';
|
||||
close(fds[0]);
|
||||
|
||||
/* reap regardless of which path brought us here */
|
||||
while (waitpid(pid, &wstatus, 0) < 0) {
|
||||
if (errno != EINTR) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (total == 0) {
|
||||
XCECD_LOG(XCE_LOG_WARN,
|
||||
"elim: command \"%s\" produced no output", command);
|
||||
return -ENODATA;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── getter plumbing: per-source cache behind the context-free API ────── */
|
||||
|
||||
/* resource_sync's getter signature is context-free (char* (*)(void)) -- the
|
||||
* per-source state (command, cached value, next-refresh deadline) lives in
|
||||
* this static array, indexed by registration order. MAX_LOAD_INDICES (64)
|
||||
* bounds the registry; the config loader's own parse bounds us to a sane
|
||||
* subset of that, and elim_register_sources' capacity failure surfaces at
|
||||
* startup. */
|
||||
typedef struct {
|
||||
char* command;
|
||||
int interval; /* seconds; 0 = refresh every collect */
|
||||
double value; /* last good parse */
|
||||
bool have_value;
|
||||
time_t next_run; /* epoch after which the script re-runs */
|
||||
} elim_source_state_t;
|
||||
|
||||
static elim_source_state_t* g_elim_states = NULL;
|
||||
static int g_elim_nstates = 0;
|
||||
static pthread_mutex_t g_elim_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static char* elim_getter_run(int slot)
|
||||
{
|
||||
elim_source_state_t* st = &g_elim_states[slot];
|
||||
char buf[4096];
|
||||
int rc;
|
||||
double v;
|
||||
char* out_str;
|
||||
time_t now;
|
||||
|
||||
/* interval cache: 0 = run the script on every collect; otherwise the
|
||||
* script re-runs only once its interval has elapsed since the last
|
||||
* good run, and the cached value is served in between (the send
|
||||
* cadence is the sync thread's 15-tick cycle regardless -- the interval
|
||||
* governs script-run frequency, not push frequency). */
|
||||
now = time(NULL);
|
||||
if (st->have_value && (st->interval > 0) && (now < st->next_run)) {
|
||||
out_str = (char*)malloc(64);
|
||||
if (out_str == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
snprintf(out_str, 64, "%f", st->value);
|
||||
return out_str;
|
||||
}
|
||||
|
||||
rc = elim_script_collect(st->command, buf, sizeof(buf));
|
||||
if (rc < 0) {
|
||||
/* Fail-open: keep the last known value (the collector died but the
|
||||
* host is still there; the sync thread keeps reporting). */
|
||||
if (!st->have_value) {
|
||||
return strdup("0");
|
||||
}
|
||||
return strdup("");
|
||||
}
|
||||
|
||||
v = elim_parse_script_output(buf);
|
||||
if (isnan(v)) {
|
||||
XCECD_LOG(XCE_LOG_WARN,
|
||||
"elim: command \"%s\" output did not parse as a value "
|
||||
"(got %.60s)", st->command, buf);
|
||||
if (!st->have_value) {
|
||||
return strdup("0");
|
||||
}
|
||||
return strdup("");
|
||||
}
|
||||
|
||||
st->value = v;
|
||||
st->have_value = true;
|
||||
st->next_run = time(NULL) + st->interval;
|
||||
|
||||
/* collect_load_indices converts per valtype (fvalue: atof+free) and
|
||||
* owns the string from here. */
|
||||
out_str = (char*)malloc(64);
|
||||
if (out_str == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
snprintf(out_str, 64, "%f", v);
|
||||
|
||||
return out_str;
|
||||
}
|
||||
|
||||
/* One trampoline per possible slot; the getter table indexes the static
|
||||
* state array. (The registry API carries no user pointer.) */
|
||||
static char* elim_getter_0(void) { return elim_getter_run(0); }
|
||||
static char* elim_getter_1(void) { return elim_getter_run(1); }
|
||||
static char* elim_getter_2(void) { return elim_getter_run(2); }
|
||||
static char* elim_getter_3(void) { return elim_getter_run(3); }
|
||||
static char* elim_getter_4(void) { return elim_getter_run(4); }
|
||||
static char* elim_getter_5(void) { return elim_getter_run(5); }
|
||||
static char* elim_getter_6(void) { return elim_getter_run(6); }
|
||||
static char* elim_getter_7(void) { return elim_getter_run(7); }
|
||||
static char* elim_getter_8(void) { return elim_getter_run(8); }
|
||||
static char* elim_getter_9(void) { return elim_getter_run(9); }
|
||||
static char* elim_getter_10(void) { return elim_getter_run(10); }
|
||||
static char* elim_getter_11(void) { return elim_getter_run(11); }
|
||||
static char* elim_getter_12(void) { return elim_getter_run(12); }
|
||||
static char* elim_getter_13(void) { return elim_getter_run(13); }
|
||||
static char* elim_getter_14(void) { return elim_getter_run(14); }
|
||||
static char* elim_getter_15(void) { return elim_getter_run(15); }
|
||||
|
||||
static char* (*const g_elim_getters[])(void) = {
|
||||
elim_getter_0, elim_getter_1, elim_getter_2, elim_getter_3,
|
||||
elim_getter_4, elim_getter_5, elim_getter_6, elim_getter_7,
|
||||
elim_getter_8, elim_getter_9, elim_getter_10, elim_getter_11,
|
||||
elim_getter_12, elim_getter_13, elim_getter_14, elim_getter_15,
|
||||
};
|
||||
|
||||
#define ELIM_MAX_SOURCES ((int)(sizeof(g_elim_getters) / sizeof(g_elim_getters[0])))
|
||||
|
||||
/* There is exactly one getter call site -- collect_load_indices() on the
|
||||
* sync thread -- but registration happens on main before that thread
|
||||
* exists; a mutex keeps the state array vs. getter table relationship
|
||||
* honest without serializing the runs themselves. */
|
||||
int elim_register_sources(const elim_config_t* cfg)
|
||||
{
|
||||
int i;
|
||||
int rc = 0;
|
||||
|
||||
if ((cfg == NULL) || (cfg->nsrc < 0)
|
||||
|| ((cfg->nsrc > 0) && (cfg->src == NULL))) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if (cfg->nsrc == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (cfg->nsrc > ELIM_MAX_SOURCES) {
|
||||
XCECD_LOG(XCE_LOG_ERROR,
|
||||
"elim: %d external resources configured, at most %d "
|
||||
"supported", cfg->nsrc, ELIM_MAX_SOURCES);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_elim_mutex);
|
||||
|
||||
/* The 11 builtins register lazily at collect time; an elim source named
|
||||
* "mem" registered before them would win the name race and SHADOW a
|
||||
* builtin (the builtin's own registration then bounces as a dup).
|
||||
* Force the builtin init first, so a name collision is a clean -EEXIST
|
||||
* here, at startup, where the operator sees it. */
|
||||
resource_sync_ensure_builtin_load_indices();
|
||||
|
||||
g_elim_states = (elim_source_state_t*)calloc(
|
||||
(size_t)cfg->nsrc, sizeof(elim_source_state_t));
|
||||
if (g_elim_states == NULL) {
|
||||
pthread_mutex_unlock(&g_elim_mutex);
|
||||
return -ENOMEM;
|
||||
}
|
||||
g_elim_nstates = cfg->nsrc;
|
||||
|
||||
for (i = 0; i < cfg->nsrc; i++) {
|
||||
if (!resource_sync_register_load_index(
|
||||
cfg->src[i].name, Value_u_fvalue, g_elim_getters[i], false)) {
|
||||
/* Duplicate name (another source, or one of the 11 builtins --
|
||||
* a user script must not shadow "mem"/"r15s"/...): surface the
|
||||
* misconfiguration at startup, not as a silent missing value. */
|
||||
XCECD_LOG(XCE_LOG_ERROR,
|
||||
"elim: cannot register external resource '%s' -- "
|
||||
"name already in use",
|
||||
cfg->src[i].name);
|
||||
rc = -EEXIST;
|
||||
break;
|
||||
}
|
||||
|
||||
g_elim_states[i].command = strdup(cfg->src[i].command);
|
||||
if (g_elim_states[i].command == NULL) {
|
||||
resource_sync_unregister_load_index(cfg->src[i].name);
|
||||
rc = -ENOMEM;
|
||||
break;
|
||||
}
|
||||
g_elim_states[i].interval = cfg->src[i].interval;
|
||||
g_elim_states[i].have_value = false;
|
||||
g_elim_states[i].next_run = 0;
|
||||
|
||||
XCECD_LOG(XCE_LOG_INFO,
|
||||
"elim: registered external resource '%s' (command: %s, "
|
||||
"interval: %ds)",
|
||||
cfg->src[i].name, g_elim_states[i].command,
|
||||
g_elim_states[i].interval);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_elim_mutex);
|
||||
|
||||
if (rc < 0) {
|
||||
elim_unregister_sources(cfg);
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
void elim_unregister_sources(const elim_config_t* cfg)
|
||||
{
|
||||
int i;
|
||||
|
||||
if ((cfg == NULL) || (cfg->src == NULL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_elim_mutex);
|
||||
|
||||
for (i = 0; i < cfg->nsrc; i++) {
|
||||
resource_sync_unregister_load_index(cfg->src[i].name);
|
||||
}
|
||||
|
||||
for (i = 0; i < g_elim_nstates; i++) {
|
||||
free(g_elim_states[i].command);
|
||||
}
|
||||
free(g_elim_states);
|
||||
g_elim_states = NULL;
|
||||
g_elim_nstates = 0;
|
||||
|
||||
pthread_mutex_unlock(&g_elim_mutex);
|
||||
}
|
||||
|
|
@ -394,10 +394,12 @@ int main(int argc, char* argv[])
|
|||
|
||||
char logfile[1024] = { 0 };
|
||||
char* logdir = NULL;
|
||||
const char* config_file = NULL;
|
||||
|
||||
static struct option long_opts[] = {
|
||||
{ "peers", required_argument, NULL, OPT_PEERS },
|
||||
{ "version", no_argument, NULL, OPT_VERSION },
|
||||
{ "config", required_argument, NULL, 'c' },
|
||||
{ "help", no_argument, NULL, 'h' },
|
||||
{ NULL, 0, NULL, 0 }
|
||||
};
|
||||
|
|
@ -418,12 +420,15 @@ int main(int argc, char* argv[])
|
|||
STR(VERSION_BUILD_TIME),
|
||||
STR(VERSION_DIRTY_FINGERPRINT));
|
||||
|
||||
while ((opt = getopt_long(argc, argv, "n:l:vh", long_opts, NULL))
|
||||
while ((opt = getopt_long(argc, argv, "n:l:c:vh", long_opts, NULL))
|
||||
!= -1) {
|
||||
switch (opt) {
|
||||
case 'n':
|
||||
config.virutal_name = optarg;
|
||||
break;
|
||||
case 'c':
|
||||
config_file = optarg;
|
||||
break;
|
||||
case 'l':
|
||||
if (optarg) {
|
||||
if (logdir) {
|
||||
|
|
@ -450,9 +455,13 @@ int main(int argc, char* argv[])
|
|||
return 0;
|
||||
case 'h':
|
||||
default:
|
||||
printf("Usage: %s [-n name] [-l log_dir] [-v] "
|
||||
printf("Usage: %s [-n name] [-l log_dir] [-c config] [-v] "
|
||||
"[--peers ctrl-1=host:port,...] "
|
||||
"[-h help]\n"
|
||||
" -c FILE elim collector config (external "
|
||||
"resources reported by\n"
|
||||
" scripts on this host; design "
|
||||
"external-license-resources §3.3b)\n"
|
||||
" --peers LIST HA controller candidate list: "
|
||||
"[name=]host:port,...\n"
|
||||
" When omitted, the XCE_CONTROLLER_PEERS "
|
||||
|
|
@ -638,6 +647,41 @@ int main(int argc, char* argv[])
|
|||
}
|
||||
#endif
|
||||
|
||||
/* elim collectors (design §3.3b): load the external_resources config
|
||||
* before xcecd_init() registers its sources. No -c (or a file without
|
||||
* the section) means zero external resources -- plain worker behavior.
|
||||
* A config that is present but BROKEN stops startup loudly: a worker
|
||||
* whose operator asked for elim reporting must not silently run
|
||||
* without it (same fail-loud-at-load rule as esd's config). */
|
||||
if (config_file != NULL) {
|
||||
elim_config_t elim_cfg;
|
||||
int erc = elim_config_load(config_file, &elim_cfg);
|
||||
|
||||
if (erc == -ENOENT) {
|
||||
XCECD_LOG(XCE_LOG_ERROR,
|
||||
"elim config file not found: %s", config_file);
|
||||
fprintf(stderr, "xcecd: elim config file not found: %s\n",
|
||||
config_file);
|
||||
FREEUP(logdir);
|
||||
return 1;
|
||||
}
|
||||
if (erc < 0) {
|
||||
XCECD_LOG(XCE_LOG_ERROR,
|
||||
"invalid elim config file %s (rc=%d) -- a source "
|
||||
"needs both name and command", config_file, erc);
|
||||
fprintf(stderr,
|
||||
"xcecd: invalid elim config file %s (rc=%d)\n",
|
||||
config_file, erc);
|
||||
FREEUP(logdir);
|
||||
return 1;
|
||||
}
|
||||
config.elim.nsrc = elim_cfg.nsrc;
|
||||
config.elim.src = elim_cfg.src; /* ownership -> xcecd_config_t */
|
||||
XCECD_LOG(XCE_LOG_INFO,
|
||||
"elim: %d external resource(s) from %s",
|
||||
config.elim.nsrc, config_file);
|
||||
}
|
||||
|
||||
if (xcecd_config_init(&config) != 0) {
|
||||
XCECD_LOG(XCE_LOG_ERROR, "Failed to initialize daemon configuration.");
|
||||
return 1;
|
||||
|
|
|
|||
|
|
@ -77,8 +77,17 @@ static void init_builtin_load_indices(void)
|
|||
* on resource_sync_thread()'s single dedicated pthread in production,
|
||||
* or single-threaded test code). */
|
||||
g_load_indices_capacity = MAX_LOAD_INDICES;
|
||||
g_load_indices = (load_index_info_t*)calloc(g_load_indices_capacity,
|
||||
sizeof(load_index_info_t));
|
||||
if (g_load_indices == NULL) {
|
||||
g_load_indices = (load_index_info_t*)calloc(g_load_indices_capacity,
|
||||
sizeof(load_index_info_t));
|
||||
}
|
||||
/* Never realloc over a populated array: since resource_sync_ensure_
|
||||
* builtin_load_indices() this function also runs with the array already
|
||||
* alive (external entries registered first -- elim), and replacing it
|
||||
* leaked the old one while leaving g_load_indices_count pointing past
|
||||
* the fresh calloc (strcmp on garbage slots, segfault). The dup check
|
||||
* in resource_sync_register_load_index() makes re-running the
|
||||
* registration list idempotent. */
|
||||
|
||||
// register built-in load indices
|
||||
resource_sync_register_load_index("r15s", Value_u_fvalue, node_info_get_15s,
|
||||
|
|
@ -105,6 +114,28 @@ static void init_builtin_load_indices(void)
|
|||
true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Ensures the registry exists AND the 11 builtin load indices are
|
||||
* registered in it, whichever external module (elim) queries or
|
||||
* registers first. Idempotent; single-threaded callers only in
|
||||
* practice (startup registration / the sync thread's collect), but
|
||||
* safe under the registry mutex discipline its own callers keep.
|
||||
*/
|
||||
void resource_sync_ensure_builtin_load_indices(void)
|
||||
{
|
||||
if (g_load_indices == NULL) {
|
||||
init_builtin_load_indices();
|
||||
return;
|
||||
}
|
||||
/* init_builtin_load_indices() ran but the array was later torn down
|
||||
* (resource_sync_destroy's cleanup) and a register-before-init caller
|
||||
* re-populated it with external entries only -- the builtins must go
|
||||
* in before the first report. */
|
||||
if (g_load_indices_count < BUILTIN_LOAD_INDICES_COUNT) {
|
||||
init_builtin_load_indices();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Registers a new load index.
|
||||
* @param name The name of the load index.
|
||||
|
|
@ -123,6 +154,27 @@ bool resource_sync_register_load_index(const char* name, int valtype,
|
|||
|
||||
pthread_mutex_lock(&g_load_indices_mutex);
|
||||
|
||||
/* elim (design §3.3b): external registration happens at daemon startup
|
||||
* -- BEFORE the sync thread's first collect_load_indices() lazily
|
||||
* allocated the array. A capacity-0 registry here used to reject every
|
||||
* such registration with "Maximum load indices capacity reached (0)",
|
||||
* silently dropping every configured external resource. Allocate the
|
||||
* registry on first use, same as the lazy builtin init does; init_
|
||||
* builtin_load_indices() itself stays where it is (collect-time) so the
|
||||
* 11 builtins keep registering exactly once per populated-registry
|
||||
* window (bug-1756's contract -- its only caller's NULL check). */
|
||||
if (g_load_indices == NULL) {
|
||||
g_load_indices_capacity = MAX_LOAD_INDICES;
|
||||
g_load_indices = (load_index_info_t*)calloc(
|
||||
g_load_indices_capacity, sizeof(load_index_info_t));
|
||||
if (g_load_indices == NULL) {
|
||||
XCECD_LOG(XCE_LOG_ERROR, "Failed to allocate load indices array");
|
||||
g_load_indices_capacity = 0;
|
||||
pthread_mutex_unlock(&g_load_indices_mutex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < g_load_indices_count; i++) {
|
||||
if (strcmp(g_load_indices[i].name, name) == 0) {
|
||||
XCECD_LOG(XCE_LOG_WARN, "Load index '%s' already exists", name);
|
||||
|
|
@ -194,9 +246,7 @@ bool resource_sync_unregister_load_index(const char* name)
|
|||
|
||||
static xce_resval_t* collect_load_indices(int* count)
|
||||
{
|
||||
if (g_load_indices == NULL) {
|
||||
init_builtin_load_indices();
|
||||
}
|
||||
resource_sync_ensure_builtin_load_indices();
|
||||
|
||||
pthread_mutex_lock(&g_load_indices_mutex);
|
||||
|
||||
|
|
|
|||
|
|
@ -395,6 +395,22 @@ int xcecd_init(const xcecd_config_t* config)
|
|||
* xcecd_start() already ran. */
|
||||
xcecd_ctx.cluster_cfg = config->cluster_cfg;
|
||||
|
||||
/* elim collectors (external-license-resources design §3.3b): register
|
||||
* the configured external resources as custom load indices BEFORE the
|
||||
* resource_sync thread starts (xcecd_start() -> resource_sync_start()),
|
||||
* so the first 15-tick report already carries them. Ownership of
|
||||
* config->elim moves here (config is read-only). */
|
||||
xcecd_ctx.elim.nsrc = config->elim.nsrc;
|
||||
xcecd_ctx.elim.src = config->elim.src;
|
||||
if (xcecd_ctx.elim.nsrc > 0) {
|
||||
if (elim_register_sources(&xcecd_ctx.elim) < 0) {
|
||||
XCECD_LOG(XCE_LOG_ERROR,
|
||||
"Failed to register elim external resources -- "
|
||||
"check external_resources config");
|
||||
goto cleanup_remote_host;
|
||||
}
|
||||
}
|
||||
|
||||
/* thread pool initializing */
|
||||
xcecd_ctx.thread_pool = thread_pool_create(config->thread_pool_size);
|
||||
if (!xcecd_ctx.thread_pool) {
|
||||
|
|
@ -457,6 +473,7 @@ cleanup_process_manager:
|
|||
cleanup_thread_pool:
|
||||
thread_pool_destroy(xcecd_ctx.thread_pool);
|
||||
cleanup_remote_host:
|
||||
elim_unregister_sources(&xcecd_ctx.elim);
|
||||
free(xcecd_ctx.remote_host);
|
||||
free(xcecd_ctx.work_dir);
|
||||
free(xcecd_ctx.db_path);
|
||||
|
|
@ -575,6 +592,9 @@ void xcecd_cleanup(void)
|
|||
resource_sync_stop(xcecd_ctx.resource_sync);
|
||||
resource_sync_destroy(xcecd_ctx.resource_sync);
|
||||
}
|
||||
/* elim teardown AFTER resource_sync_destroy (which tears the registry
|
||||
* down anyway -- this frees the source state and command strings). */
|
||||
elim_unregister_sources(&xcecd_ctx.elim);
|
||||
if (xcecd_ctx.db) {
|
||||
xcecd_db_close(xcecd_ctx.db);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,23 @@ test('worker resource_sync test', resource_sync_test,
|
|||
timeout: 60,
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
|
||||
# external-license-resources design §3.3b: the elim collector -- a
|
||||
# config-driven script runner (posix_spawn + watchdog) that registers
|
||||
# user-defined per-host load indices into resource_sync's registry at
|
||||
# worker startup. White-box #includes elim.c AND resource_sync.c (the
|
||||
# registry capacity-0-before-init trap is only observable against the real
|
||||
# registry); see test_elim.c's header comment.
|
||||
elim_test = executable('elim_test',
|
||||
'test_elim.c',
|
||||
include_directories: [worker_test_incdir],
|
||||
dependencies: worker_test_deps,
|
||||
install: false)
|
||||
|
||||
test('worker elim test', elim_test,
|
||||
suite: 'worker',
|
||||
timeout: 60,
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
|
||||
# bug-1554: real maxswap (sysinfo() totalswap)/maxtmp (statvfs("/tmp"))
|
||||
# detection -- white-box #include of hardware_monitor.c, same convention as
|
||||
# resource_sync_test above.
|
||||
|
|
|
|||
566
src/worker/testsuite/test_elim.c
Normal file
566
src/worker/testsuite/test_elim.c
Normal file
|
|
@ -0,0 +1,566 @@
|
|||
/*
|
||||
* test_elim.c -- elim collector module (external-license-resources design
|
||||
* §3.3b): xcecd gains a config-driven script collector that registers
|
||||
* user-defined load indices into resource_sync's registry (the previously
|
||||
* dead external-register API), so per-host "external resources" reported by
|
||||
* user scripts on each host flow the live load-indices push chain
|
||||
* (collect_load_indices -> xcecd_send_load_indices -> controller
|
||||
* update_val_res -> scheduler RESVALS_DELTA -> sched_check_host_satisfy_).
|
||||
*
|
||||
* White-box #include of ../src/elim.c (same convention as
|
||||
* test_resource_sync.c) plus ../src/esd_script-shaped spawn code lives in
|
||||
* elim.c itself; the config parse is exercised against real temp files
|
||||
* (esd_test's mkdtemp pattern). The spawn/watchdog half IS unit tested here
|
||||
* (unlike esd's test, which deferred it to E2E): a real /bin/sh child with
|
||||
* a real timeout kill is deterministic and cheap, and the watchdog is the
|
||||
* one piece of elim.c that can hang the sync thread if it regresses.
|
||||
*
|
||||
* resource_sync.c is ALSO #included (after elim.c) to exercise the real
|
||||
* registry rather than a stub: the capacity-0-before-init trap this phase
|
||||
* fixes (elim registers at startup, before the sync thread's first collect
|
||||
* lazily allocated the array) is only observable against the real code.
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "resource_sync.h"
|
||||
#include "xcecd.h"
|
||||
|
||||
/* Real globals the white-box resource_sync.c references (test_resource_sync.c
|
||||
* convention). */
|
||||
xcecd_context_t xcecd_ctx;
|
||||
|
||||
/* Stubs for the 11 builtin load-index getters (node_information.c, not
|
||||
* linked here). */
|
||||
static char* stub_getter(void)
|
||||
{
|
||||
return strdup("1.0");
|
||||
}
|
||||
|
||||
char* node_info_get_15s(void) { return stub_getter(); }
|
||||
char* node_info_get_1m(void) { return stub_getter(); }
|
||||
char* node_info_get_15m(void) { return stub_getter(); }
|
||||
char* node_info_get_ut(void) { return stub_getter(); }
|
||||
char* node_info_get_pg(void) { return stub_getter(); }
|
||||
char* node_info_get_ls(void) { return stub_getter(); }
|
||||
char* node_info_get_it(void) { return stub_getter(); }
|
||||
char* node_info_get_swap(void) { return stub_getter(); }
|
||||
char* node_info_get_mem(void) { return stub_getter(); }
|
||||
char* node_info_get_tmp(void) { return stub_getter(); }
|
||||
char* node_info_get_io(void) { return stub_getter(); }
|
||||
|
||||
/* Spy on the send (controller_connector.c, not linked here). */
|
||||
static int g_send_load_indices_call_count = 0;
|
||||
|
||||
bool xcecd_send_load_indices(xce_resval_t** vals, int count)
|
||||
{
|
||||
(void)vals;
|
||||
(void)count;
|
||||
g_send_load_indices_call_count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* resource_sync.c's resync gate (xcecd_resync.c, not linked here). */
|
||||
static bool g_test_resync_pending = false;
|
||||
|
||||
bool xcecd_resync_is_pending(void) { return g_test_resync_pending; }
|
||||
|
||||
#include "../src/elim.c"
|
||||
#include "../src/resource_sync.c"
|
||||
|
||||
static char g_tmpdir[256];
|
||||
|
||||
void setUp(void)
|
||||
{
|
||||
snprintf(g_tmpdir, sizeof(g_tmpdir), "/tmp/xce_elim_testXXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(g_tmpdir));
|
||||
|
||||
memset(&xcecd_ctx, 0, sizeof(xcecd_ctx));
|
||||
xcecd_ctx.virtual_name = (char*)"t1";
|
||||
g_test_resync_pending = false;
|
||||
g_send_load_indices_call_count = 0;
|
||||
}
|
||||
|
||||
void tearDown(void)
|
||||
{
|
||||
/* Drop any registrations this test left so the next test starts from a
|
||||
* clean registry: resource_sync_destroy on a throwaway object runs
|
||||
* resource_sync_cleanup_load_indices() (static in the #included
|
||||
* resource_sync.c, reachable only through this public entry). */
|
||||
resource_sync_destroy(resource_sync_create());
|
||||
|
||||
if (g_tmpdir[0] != '\0') {
|
||||
char path[300];
|
||||
|
||||
snprintf(path, sizeof(path), "rm -rf %s", g_tmpdir);
|
||||
if (system(path) != 0) {
|
||||
/* best effort */
|
||||
}
|
||||
g_tmpdir[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static void write_file(const char* dir, const char* name, const char* content)
|
||||
{
|
||||
char path[512];
|
||||
FILE* f;
|
||||
|
||||
snprintf(path, sizeof(path), "%s/%s", dir, name);
|
||||
f = fopen(path, "w");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
fputs(content, f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* ── elim config parse ────────────────────────────────────────────────── */
|
||||
|
||||
static void test_elim_config_load_full_file(void)
|
||||
{
|
||||
elim_config_t cfg;
|
||||
int rc;
|
||||
|
||||
write_file(g_tmpdir, "elim.yaml",
|
||||
"external_resources:\n"
|
||||
" - name: dynres1\n"
|
||||
" command: /opt/xce/scripts/report-dynres1.sh\n"
|
||||
" interval: 30\n"
|
||||
" - name: gpu_free\n"
|
||||
" command: /usr/local/bin/gpu-free.sh\n"
|
||||
" interval: 60\n");
|
||||
|
||||
rc = elim_config_load_path(g_tmpdir, "elim.yaml", &cfg);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(2, cfg.nsrc);
|
||||
TEST_ASSERT_EQUAL_STRING("dynres1", cfg.src[0].name);
|
||||
TEST_ASSERT_EQUAL_STRING("/opt/xce/scripts/report-dynres1.sh",
|
||||
cfg.src[0].command);
|
||||
TEST_ASSERT_EQUAL_INT(30, cfg.src[0].interval);
|
||||
TEST_ASSERT_EQUAL_STRING("gpu_free", cfg.src[1].name);
|
||||
TEST_ASSERT_EQUAL_INT(60, cfg.src[1].interval);
|
||||
|
||||
elim_config_fini(&cfg);
|
||||
}
|
||||
|
||||
static void test_elim_config_interval_defaults_when_missing(void)
|
||||
{
|
||||
elim_config_t cfg;
|
||||
int rc;
|
||||
|
||||
write_file(g_tmpdir, "elim.yaml",
|
||||
"external_resources:\n"
|
||||
" - name: dynres1\n"
|
||||
" command: /opt/xce/scripts/r.sh\n");
|
||||
|
||||
rc = elim_config_load_path(g_tmpdir, "elim.yaml", &cfg);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, cfg.nsrc);
|
||||
/* 0 = "every send tick" -- the getter cache refreshes each time the
|
||||
* sync thread collects. */
|
||||
TEST_ASSERT_EQUAL_INT(0, cfg.src[0].interval);
|
||||
|
||||
elim_config_fini(&cfg);
|
||||
}
|
||||
|
||||
static void test_elim_config_missing_file_is_enoent(void)
|
||||
{
|
||||
elim_config_t cfg;
|
||||
int rc;
|
||||
|
||||
rc = elim_config_load_path(g_tmpdir, "no-such-file.yaml", &cfg);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-ENOENT, rc);
|
||||
}
|
||||
|
||||
static void test_elim_config_missing_command_is_einval(void)
|
||||
{
|
||||
elim_config_t cfg;
|
||||
int rc;
|
||||
|
||||
/* A source without a command can never report -- fail loudly at load,
|
||||
* not silently at runtime (esd_config.c's validation rule). */
|
||||
write_file(g_tmpdir, "elim.yaml",
|
||||
"external_resources:\n"
|
||||
" - name: dynres1\n"
|
||||
" interval: 30\n");
|
||||
|
||||
rc = elim_config_load_path(g_tmpdir, "elim.yaml", &cfg);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
||||
}
|
||||
|
||||
static void test_elim_config_missing_name_is_einval(void)
|
||||
{
|
||||
elim_config_t cfg;
|
||||
int rc;
|
||||
|
||||
write_file(g_tmpdir, "elim.yaml",
|
||||
"external_resources:\n"
|
||||
" - command: /opt/xce/scripts/r.sh\n");
|
||||
|
||||
rc = elim_config_load_path(g_tmpdir, "elim.yaml", &cfg);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
||||
}
|
||||
|
||||
static void test_elim_config_empty_or_absent_section_is_ok(void)
|
||||
{
|
||||
elim_config_t cfg;
|
||||
int rc;
|
||||
|
||||
/* A worker config with no external_resources key is the normal case for
|
||||
* every host without elim scripts -- must load to zero sources, not
|
||||
* error. */
|
||||
write_file(g_tmpdir, "elim.yaml", "external_resources: []\n");
|
||||
|
||||
rc = elim_config_load_path(g_tmpdir, "elim.yaml", &cfg);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, cfg.nsrc);
|
||||
TEST_ASSERT_NULL(cfg.src);
|
||||
|
||||
elim_config_fini(&cfg);
|
||||
}
|
||||
|
||||
/* ── elim output parse ────────────────────────────────────────────────── */
|
||||
|
||||
static void test_elim_parse_output_name_value_line(void)
|
||||
{
|
||||
double v;
|
||||
|
||||
/* The elim contract: first non-empty line carries "<name> <value>". */
|
||||
v = elim_parse_script_output("dynres1 42\n");
|
||||
TEST_ASSERT_FLOAT_WITHIN(1e-6, 42.0, v);
|
||||
|
||||
v = elim_parse_script_output("\n\n# comment-ish noise 7 8\ngpu_free 5.5\n");
|
||||
TEST_ASSERT_FLOAT_WITHIN(1e-6, 7.0, v);
|
||||
}
|
||||
|
||||
static void test_elim_parse_output_bare_value_line(void)
|
||||
{
|
||||
double v;
|
||||
|
||||
v = elim_parse_script_output("12\n");
|
||||
TEST_ASSERT_FLOAT_WITHIN(1e-6, 12.0, v);
|
||||
|
||||
v = elim_parse_script_output(" 3.5 \n");
|
||||
TEST_ASSERT_FLOAT_WITHIN(1e-6, 3.5, v);
|
||||
}
|
||||
|
||||
static void test_elim_parse_output_garbage_is_nan(void)
|
||||
{
|
||||
TEST_ASSERT(isnan(elim_parse_script_output("no numbers here\n")));
|
||||
TEST_ASSERT(isnan(elim_parse_script_output("")));
|
||||
TEST_ASSERT(isnan(elim_parse_script_output(NULL)));
|
||||
}
|
||||
|
||||
/* ── registry wiring: register at startup, before any collect ─────────── */
|
||||
|
||||
/* THE trap this phase fixes: before it, resource_sync_register_load_index
|
||||
* called at daemon startup (when g_load_indices is still NULL -- the array
|
||||
* is only allocated by collect_load_indices' lazy init_builtin_load_indices,
|
||||
* i.e. on the sync thread's first 15th tick) failed with "capacity reached
|
||||
* (0)" and every elim source silently vanished. Registration must
|
||||
* self-initialize. */
|
||||
static void test_register_load_index_before_lazy_init_succeeds(void)
|
||||
{
|
||||
bool ok;
|
||||
|
||||
/* No collect_load_indices() call ever happened in this process yet. */
|
||||
TEST_ASSERT_NULL_MESSAGE(g_load_indices,
|
||||
"precondition: registry not yet initialized");
|
||||
|
||||
ok = resource_sync_register_load_index("dynres1", Value_u_fvalue,
|
||||
stub_getter, false);
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
ok, "registering before the lazy builtin init must not fail with "
|
||||
"'capacity reached (0)'");
|
||||
|
||||
/* And the value must actually flow: collect_load_indices now includes
|
||||
* the elim index among the builtins. */
|
||||
xce_resval_t* vals;
|
||||
int count = 0;
|
||||
int found = 0;
|
||||
|
||||
vals = collect_load_indices(&count);
|
||||
TEST_ASSERT_NOT_NULL(vals);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (strcmp(vals[i].name, "dynres1") == 0) {
|
||||
found++;
|
||||
TEST_ASSERT_EQUAL_STRING("t1", vals[i].host);
|
||||
TEST_ASSERT_EQUAL_INT(Value_u_fvalue, vals[i].val->which);
|
||||
TEST_ASSERT_FLOAT_WITHIN(1e-6, 1.0, vals[i].val->u.fvalue);
|
||||
}
|
||||
/* The host tag is the join identity for every report (bug-1569). */
|
||||
TEST_ASSERT_EQUAL_STRING("t1", vals[i].host);
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT(1, found);
|
||||
|
||||
free_resvals(vals, count);
|
||||
}
|
||||
|
||||
static void test_elim_sources_register_and_flow_through_registry(void)
|
||||
{
|
||||
elim_config_t cfg;
|
||||
int rc;
|
||||
|
||||
write_file(g_tmpdir, "elim.yaml",
|
||||
"external_resources:\n"
|
||||
" - name: dynres1\n"
|
||||
" command: echo dynres1 7\n"
|
||||
" interval: 0\n");
|
||||
|
||||
rc = elim_config_load_path(g_tmpdir, "elim.yaml", &cfg);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
rc = elim_register_sources(&cfg);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* The getter runs the script: first collect refreshes the cache. */
|
||||
xce_resval_t* vals;
|
||||
int count = 0;
|
||||
int found = 0;
|
||||
|
||||
vals = collect_load_indices(&count);
|
||||
TEST_ASSERT_NOT_NULL(vals);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (strcmp(vals[i].name, "dynres1") == 0) {
|
||||
found++;
|
||||
TEST_ASSERT_EQUAL_INT(Value_u_fvalue, vals[i].val->which);
|
||||
/* "echo dynres1 7" -> name-value line -> 7.0 */
|
||||
TEST_ASSERT_FLOAT_WITHIN(1e-6, 7.0, vals[i].val->u.fvalue);
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
1, found, "the elim-registered index must reach collect_load_indices");
|
||||
|
||||
free_resvals(vals, count);
|
||||
}
|
||||
|
||||
static void test_elim_register_rejects_duplicate_name(void)
|
||||
{
|
||||
elim_config_t cfg;
|
||||
int rc;
|
||||
|
||||
write_file(g_tmpdir, "elim.yaml",
|
||||
"external_resources:\n"
|
||||
" - name: dynres1\n"
|
||||
" command: echo dynres1 1\n"
|
||||
" - name: dynres1\n"
|
||||
" command: echo dynres1 2\n");
|
||||
|
||||
rc = elim_config_load_path(g_tmpdir, "elim.yaml", &cfg);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* The registry rejects the dup; elim_register_sources reports the
|
||||
* failure so the operator sees the misconfiguration at startup. */
|
||||
TEST_ASSERT_EQUAL_INT(-EEXIST, elim_register_sources(&cfg));
|
||||
}
|
||||
|
||||
static void test_elim_register_rejects_builtin_name_override(void)
|
||||
{
|
||||
elim_config_t cfg;
|
||||
int rc;
|
||||
|
||||
/* "mem" is one of the 11 builtins -- a user script must not shadow it
|
||||
* (the registry rejects the dup; the error surfaces at startup). */
|
||||
write_file(g_tmpdir, "elim.yaml",
|
||||
"external_resources:\n"
|
||||
" - name: mem\n"
|
||||
" command: echo mem 999\n");
|
||||
|
||||
rc = elim_config_load_path(g_tmpdir, "elim.yaml", &cfg);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-EEXIST, elim_register_sources(&cfg));
|
||||
}
|
||||
|
||||
static void test_elim_unregister_sources_cleans_up(void)
|
||||
{
|
||||
elim_config_t cfg;
|
||||
int rc;
|
||||
|
||||
write_file(g_tmpdir, "elim.yaml",
|
||||
"external_resources:\n"
|
||||
" - name: dynres1\n"
|
||||
" command: echo dynres1 1\n");
|
||||
|
||||
rc = elim_config_load_path(g_tmpdir, "elim.yaml", &cfg);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, elim_register_sources(&cfg));
|
||||
TEST_ASSERT_TRUE(resource_sync_unregister_load_index("dynres1"));
|
||||
TEST_ASSERT_FALSE(resource_sync_unregister_load_index("dynres1"));
|
||||
}
|
||||
|
||||
/* ── the script runner: real child, real watchdog ─────────────────────── */
|
||||
|
||||
static void test_elim_script_collect_runs_command(void)
|
||||
{
|
||||
char out[128];
|
||||
int rc;
|
||||
const char* script;
|
||||
|
||||
script = "/bin/sh -c 'echo dynres1 42'";
|
||||
rc = elim_script_collect(script, out, sizeof(out));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_FLOAT_WITHIN(1e-6, 42.0, elim_parse_script_output(out));
|
||||
}
|
||||
|
||||
static void test_elim_script_collect_timeout_kills_child(void)
|
||||
{
|
||||
char out[128];
|
||||
int rc;
|
||||
int saved_timeout;
|
||||
const char* script;
|
||||
|
||||
/* A script that hangs must be killed at the watchdog deadline, not
|
||||
* stall the sync thread forever (the esd_script watchdog contract). */
|
||||
script = "/bin/sh -c 'sleep 60'";
|
||||
saved_timeout = elim_script_timeout_ms;
|
||||
elim_script_timeout_ms = 500;
|
||||
|
||||
rc = elim_script_collect(script, out, sizeof(out));
|
||||
|
||||
elim_script_timeout_ms = saved_timeout;
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(-ETIMEDOUT, rc,
|
||||
"a hung script must hit the watchdog");
|
||||
|
||||
/* And the child must actually be reaped -- no zombie left behind. */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(-1, waitpid(-1, NULL, WNOHANG),
|
||||
"no unreaped child may survive the kill");
|
||||
}
|
||||
|
||||
static void test_elim_script_collect_empty_output_is_enodata(void)
|
||||
{
|
||||
char out[128];
|
||||
int rc;
|
||||
|
||||
rc = elim_script_collect("/bin/sh -c 'exit 0'", out, sizeof(out));
|
||||
TEST_ASSERT_EQUAL_INT(-ENODATA, rc);
|
||||
}
|
||||
|
||||
/* ── getter cache semantics: interval ─────────────────────────────────── */
|
||||
|
||||
static void test_elim_getter_refreshes_per_interval(void)
|
||||
{
|
||||
elim_config_t cfg;
|
||||
xce_resval_t* vals;
|
||||
int count = 0;
|
||||
static char counter_script[512];
|
||||
static char yaml[1024];
|
||||
int v1 = -1, v2 = -1;
|
||||
resource_sync_t* sync;
|
||||
|
||||
/* A script whose output changes on every run: the getter must return
|
||||
* the FRESH value after the interval elapses (refresh), and the cached
|
||||
* one before it (no re-run). */
|
||||
snprintf(counter_script, sizeof(counter_script), "%s/counter.sh", g_tmpdir);
|
||||
{
|
||||
FILE* f = fopen(counter_script, "w");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
fprintf(f,
|
||||
"#!/bin/sh\n"
|
||||
"if [ -f %s.n ]; then n=$(cat %s.n); n=$((n+1)); else n=1; fi\n"
|
||||
"echo $n > %s.n\n"
|
||||
"echo \"dynres1 $n\"\n",
|
||||
counter_script, counter_script, counter_script);
|
||||
fclose(f);
|
||||
chmod(counter_script, 0755);
|
||||
}
|
||||
|
||||
snprintf(yaml, sizeof(yaml),
|
||||
"external_resources:\n"
|
||||
" - name: dynres1\n"
|
||||
" command: %s\n"
|
||||
" interval: 3\n",
|
||||
counter_script);
|
||||
write_file(g_tmpdir, "elim.yaml", yaml);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, elim_config_load_path(g_tmpdir, "elim.yaml", &cfg));
|
||||
TEST_ASSERT_EQUAL_INT(0, elim_register_sources(&cfg));
|
||||
|
||||
/* First collect: getter runs the script (counter=1). */
|
||||
vals = collect_load_indices(&count);
|
||||
TEST_ASSERT_NOT_NULL(vals);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (strcmp(vals[i].name, "dynres1") == 0) {
|
||||
v1 = (int)vals[i].val->u.fvalue;
|
||||
}
|
||||
}
|
||||
free_resvals(vals, count);
|
||||
TEST_ASSERT_EQUAL_INT(1, v1);
|
||||
|
||||
/* Collect again immediately (interval=3 not yet elapsed): the cached
|
||||
* value is returned, the counter script does NOT advance. */
|
||||
vals = collect_load_indices(&count);
|
||||
TEST_ASSERT_NOT_NULL(vals);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (strcmp(vals[i].name, "dynres1") == 0) {
|
||||
v2 = (int)vals[i].val->u.fvalue;
|
||||
}
|
||||
}
|
||||
free_resvals(vals, count);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
1, v2, "getter must serve the cached value within its interval");
|
||||
|
||||
/* After the interval elapses the getter re-runs the script. Drive it
|
||||
* through a real resource_sync tick loop rather than sleeping 3s in
|
||||
* wall-clock: seed the counter and run two 1s ticks. */
|
||||
sync = resource_sync_create();
|
||||
TEST_ASSERT_NOT_NULL(sync);
|
||||
sync->load_report_counter = LOAD_INDICES_INTERVAL_DEFAULT - 1;
|
||||
TEST_ASSERT_TRUE(resource_sync_start(sync));
|
||||
/* Two ticks cover the 1s sleep-per-tick of the sync thread; interval 3
|
||||
* seconds measured from the first run has now elapsed. */
|
||||
sleep(4);
|
||||
|
||||
vals = collect_load_indices(&count);
|
||||
TEST_ASSERT_NOT_NULL(vals);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (strcmp(vals[i].name, "dynres1") == 0) {
|
||||
v2 = (int)vals[i].val->u.fvalue;
|
||||
}
|
||||
}
|
||||
free_resvals(vals, count);
|
||||
|
||||
resource_sync_stop(sync);
|
||||
resource_sync_destroy(sync);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
2, v2, "getter must refresh the value once its interval elapses");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_elim_config_load_full_file);
|
||||
RUN_TEST(test_elim_config_interval_defaults_when_missing);
|
||||
RUN_TEST(test_elim_config_missing_file_is_enoent);
|
||||
RUN_TEST(test_elim_config_missing_command_is_einval);
|
||||
RUN_TEST(test_elim_config_missing_name_is_einval);
|
||||
RUN_TEST(test_elim_config_empty_or_absent_section_is_ok);
|
||||
RUN_TEST(test_elim_parse_output_name_value_line);
|
||||
RUN_TEST(test_elim_parse_output_bare_value_line);
|
||||
RUN_TEST(test_elim_parse_output_garbage_is_nan);
|
||||
RUN_TEST(test_register_load_index_before_lazy_init_succeeds);
|
||||
RUN_TEST(test_elim_sources_register_and_flow_through_registry);
|
||||
RUN_TEST(test_elim_register_rejects_duplicate_name);
|
||||
RUN_TEST(test_elim_register_rejects_builtin_name_override);
|
||||
RUN_TEST(test_elim_unregister_sources_cleans_up);
|
||||
RUN_TEST(test_elim_script_collect_runs_command);
|
||||
RUN_TEST(test_elim_script_collect_timeout_kills_child);
|
||||
RUN_TEST(test_elim_script_collect_empty_output_is_enodata);
|
||||
RUN_TEST(test_elim_getter_refreshes_per_interval);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
@ -161,6 +161,17 @@ bool resource_sync_start(resource_sync_t* sync)
|
|||
}
|
||||
void resource_sync_stop(resource_sync_t* sync) { (void)sync; }
|
||||
|
||||
/* elim surface (elim.c, not linked here -- this test stubs every subsystem
|
||||
* constructor the same way): xcecd_init() registers the configured sources
|
||||
* (zero sources in this test's configs, so the real impl would no-op, but
|
||||
* the symbol must resolve); xcecd_cleanup() unregisters. */
|
||||
int elim_register_sources(const elim_config_t* cfg)
|
||||
{
|
||||
(void)cfg;
|
||||
return 0;
|
||||
}
|
||||
void elim_unregister_sources(const elim_config_t* cfg) { (void)cfg; }
|
||||
|
||||
/* Referenced by xcecd_process_command()/xcecd_cleanup() -- never invoked at
|
||||
* runtime by this test (only xcecd_init()/xcecd_start() are called), but
|
||||
* their bodies are compiled unconditionally so the symbols must resolve. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue