From 7aa00f0d9d23a6918b89f4333671e5ead527e810 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Thu, 1 Oct 2026 10:34:59 +0000 Subject: [PATCH] feat(job-array-one-based-index): XCE_JOBINDEX is the element number (task 3.2) The worker sets XCE_JOBINDEX to the element number for jobs carrying the array marking (a one-element array's element gets 1) and to 0 otherwise. A submission-supplied XCE_JOBINDEX is not applied and a WARN names the job and the ignored variable; XCE_JOBID handling is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../specs/job-array-one-based-index/tasks.md | 2 +- src/worker/src/xcecd_exdata.c | 18 +- src/worker/testsuite/meson.build | 13 + src/worker/testsuite/test_exdata_jobindex.c | 240 ++++++++++++++++++ 4 files changed, 269 insertions(+), 4 deletions(-) create mode 100644 src/worker/testsuite/test_exdata_jobindex.c diff --git a/.kiro/specs/job-array-one-based-index/tasks.md b/.kiro/specs/job-array-one-based-index/tasks.md index adf98fc9..dc5a6145 100644 --- a/.kiro/specs/job-array-one-based-index/tasks.md +++ b/.kiro/specs/job-array-one-based-index/tasks.md @@ -59,7 +59,7 @@ - _Boundary: SchedGuard_ - _Depends: 1.1_ -- [ ] 3.2 (P) Expose the element number to running jobs +- [x] 3.2 (P) Expose the element number to running jobs - Set `XCE_JOBINDEX` to the element number for jobs carrying the array marking and to 0 otherwise - Skip a submission-supplied `XCE_JOBINDEX`, logging a WARN that names the job and the ignored variable; leave `XCE_JOBID` handling unchanged - Add the worker test and register it in the worker test build diff --git a/src/worker/src/xcecd_exdata.c b/src/worker/src/xcecd_exdata.c index f08c574b..acee8f8d 100644 --- a/src/worker/src/xcecd_exdata.c +++ b/src/worker/src/xcecd_exdata.c @@ -416,8 +416,11 @@ void apply_env_vars(env_var_t** env_vars, uint64_t jobid) jobid_str); } - char jobindex_str[8]; - snprintf(jobindex_str, sizeof(jobindex_str), "%d", 0); + /* Element number of an array element (6.1, 7.8); 0 otherwise (6.2). */ + uint64_t jobindex = is_array_tag(jobid) ? array_idx(jobid) : 0; + char jobindex_str[8]; + snprintf(jobindex_str, sizeof(jobindex_str), "%lu", + (unsigned long)jobindex); if (setenv("XCE_JOBINDEX", jobindex_str, 1) != 0) { XCECD_WORKER_LOG(XCE_LOG_ERROR, "Error setting environment variable: %s", @@ -431,7 +434,16 @@ void apply_env_vars(env_var_t** env_vars, uint64_t jobid) while (current != NULL) { next_node = current->next; - if (current->key && current->value) { + if (current->key && current->value + && strcmp(current->key, "XCE_JOBINDEX") == 0) { + /* The system value cannot be overridden (6.3, 6.4). */ + char idstr[64]; + fmt_jobid(idstr, sizeof(idstr), jobid); + XCECD_WORKER_LOG(XCE_LOG_WARN, + "Job %s: ignoring submitted environment " + "variable XCE_JOBINDEX (set by XCE)", + idstr); + } else if (current->key && current->value) { // Set environment variable (overwrite if exists) if (setenv(current->key, current->value, 1) != 0) { XCECD_WORKER_LOG(XCE_LOG_ERROR, diff --git a/src/worker/testsuite/meson.build b/src/worker/testsuite/meson.build index d4b9d495..9d435798 100644 --- a/src/worker/testsuite/meson.build +++ b/src/worker/testsuite/meson.build @@ -219,3 +219,16 @@ test('worker diskstats test', diskstats_test, suite: 'worker', timeout: 60, env: ['ASAN_OPTIONS=detect_leaks=0']) + +# job-array-one-based-index task 3.2 (Requirements 6.1-6.5, 7.8): XCE_JOBINDEX +# in a running job's environment -- white-box #include of xcecd_exdata.c. +exdata_jobindex_test = executable('exdata_jobindex_test', + 'test_exdata_jobindex.c', + include_directories: [worker_test_incdir], + dependencies: worker_test_deps, + install: false) + +test('worker exdata_jobindex test', exdata_jobindex_test, + suite: 'worker', + timeout: 60, + env: ['ASAN_OPTIONS=detect_leaks=0']) diff --git a/src/worker/testsuite/test_exdata_jobindex.c b/src/worker/testsuite/test_exdata_jobindex.c new file mode 100644 index 00000000..b6f33773 --- /dev/null +++ b/src/worker/testsuite/test_exdata_jobindex.c @@ -0,0 +1,240 @@ +/* + * test_exdata_jobindex.c -- XCE_JOBINDEX in a running job's environment + * (job-array-one-based-index, task 3.2; Requirements 6.1-6.5, 7.8). + * + * apply_env_vars() (xcecd_exdata.c) sets XCE_JOBID and XCE_JOBINDEX and then + * the submission's own variables. XCE_JOBINDEX must be the element number of + * an array-tagged job ID (a one-element array included) and 0 for an + * ordinary job; a submission-supplied XCE_JOBINDEX is ignored with a WARN + * naming the job and the variable; XCE_JOBID handling is unchanged. + * + * White-box #include of ../src/xcecd_exdata.c (same convention as the other + * tests in this suite). XCECD_WORKER_LOG is redefined to a capture hook + * before the include: xcelog.h is include-guarded, so the production file's + * own #include "xcelog.h" does not restore the original macro. + */ +#include +#include +#include +#include + +#include + +#include "config.h" +#include "utils.h" +#include "xcelog.h" + +#define CAPTURE_MAX 32 + +static int captured_level[CAPTURE_MAX]; +static char captured_msg[CAPTURE_MAX][512]; +static int captured_count; + +static void capture_log(int level, const char* fmt, ...) +{ + va_list ap; + + if (captured_count >= CAPTURE_MAX) { + return; + } + captured_level[captured_count] = level; + va_start(ap, fmt); + vsnprintf(captured_msg[captured_count], + sizeof(captured_msg[captured_count]), fmt, ap); + va_end(ap); + captured_count++; +} + +#undef XCECD_WORKER_LOG +#define XCECD_WORKER_LOG(level, format, ...) \ + capture_log((int)(level), format __VA_OPT__(, ) __VA_ARGS__) + +#include "../src/xcecd_exdata.c" + +/* cluster 1, apiserver 2, timestamp 3, sequence block 1 (seq 1 << 15) */ +#define TEST_SEQ_BLOCK ((uint64_t)1 << N_ARRAY_IDX_BITS) +#define ARRAY_ELEMENT(k) \ + MAKE_JOBID(1, 2, 3, TAG_JOB_ARRAY, TEST_SEQ_BLOCK | (uint64_t)(k)) +#define ORDINARY_JOB MAKE_JOBID(1, 2, 3, 0, 77) + +static env_var_t* push_var(env_var_t* head, const char* key, const char* value) +{ + env_var_t* node = calloc(1, sizeof(*node)); + + TEST_ASSERT_NOT_NULL(node); + node->key = strdup(key); + node->value = strdup(value); + node->next = head; + return node; +} + +/* Index of the first captured WARN containing every needle, or -1. */ +static int find_warn(const char* needle1, const char* needle2) +{ + for (int i = 0; i < captured_count; i++) { + if (captured_level[i] == XCE_LOG_WARN + && strstr(captured_msg[i], needle1) != NULL + && strstr(captured_msg[i], needle2) != NULL) { + return i; + } + } + return -1; +} + +static int count_warns(void) +{ + int n = 0; + + for (int i = 0; i < captured_count; i++) { + if (captured_level[i] == XCE_LOG_WARN) { + n++; + } + } + return n; +} + +static void expect_env(const char* name, const char* value) +{ + const char* got = getenv(name); + + TEST_ASSERT_NOT_NULL_MESSAGE(got, name); + TEST_ASSERT_EQUAL_STRING_MESSAGE(value, got, name); +} + +void setUp(void) +{ + captured_count = 0; + unsetenv("XCE_JOBID"); + unsetenv("XCE_JOBINDEX"); + unsetenv("XCE_TEST_USERVAR"); +} + +void tearDown(void) { return; } + +/* 6.1: an array element sees its own element number. */ +static void test_array_element_gets_its_number(void) +{ + env_var_t* vars = NULL; + + apply_env_vars(&vars, ARRAY_ELEMENT(3)); + expect_env("XCE_JOBINDEX", "3"); + + apply_env_vars(&vars, ARRAY_ELEMENT(32767)); + expect_env("XCE_JOBINDEX", "32767"); +} + +/* 7.8: a one-element array's element behaves like any element: number 1. */ +static void test_one_element_array_gets_one(void) +{ + env_var_t* vars = NULL; + + apply_env_vars(&vars, ARRAY_ELEMENT(1)); + expect_env("XCE_JOBINDEX", "1"); +} + +/* 6.2: an ordinary job gets 0 even if its low bits are non-zero. */ +static void test_ordinary_job_gets_zero(void) +{ + env_var_t* vars = NULL; + + apply_env_vars(&vars, ORDINARY_JOB); + expect_env("XCE_JOBINDEX", "0"); +} + +/* A pre-upgrade element 0 (tagged, index 0) is still reported as 0. */ +static void test_preupgrade_element_zero_gets_zero(void) +{ + env_var_t* vars = NULL; + + apply_env_vars(&vars, ARRAY_ELEMENT(0)); + expect_env("XCE_JOBINDEX", "0"); +} + +/* 6.3, 6.4: a submission's XCE_JOBINDEX is ignored with a WARN naming the + * job and the variable; other user variables are still applied. */ +static void test_user_jobindex_ignored_with_warn(void) +{ + env_var_t* vars = NULL; + char idstr[64]; + + vars = push_var(vars, "XCE_TEST_USERVAR", "kept"); + vars = push_var(vars, "XCE_JOBINDEX", "99"); + + apply_env_vars(&vars, ARRAY_ELEMENT(5)); + + TEST_ASSERT_NULL(vars); + expect_env("XCE_JOBINDEX", "5"); + expect_env("XCE_TEST_USERVAR", "kept"); + + fmt_jobid(idstr, sizeof(idstr), ARRAY_ELEMENT(5)); + TEST_ASSERT_TRUE_MESSAGE(find_warn(idstr, "XCE_JOBINDEX") >= 0, + "no WARN naming the job and XCE_JOBINDEX"); + TEST_ASSERT_EQUAL_INT(1, count_warns()); +} + +/* 6.3: an ordinary job's user XCE_JOBINDEX is ignored too (stays 0). */ +static void test_user_jobindex_ignored_for_ordinary_job(void) +{ + env_var_t* vars = NULL; + char idstr[64]; + + vars = push_var(vars, "XCE_JOBINDEX", ""); + apply_env_vars(&vars, ORDINARY_JOB); + + expect_env("XCE_JOBINDEX", "0"); + fmt_jobid(idstr, sizeof(idstr), ORDINARY_JOB); + TEST_ASSERT_TRUE(find_warn(idstr, "XCE_JOBINDEX") >= 0); +} + +/* Only the exact name is ignored. */ +static void test_similar_names_still_applied(void) +{ + env_var_t* vars = NULL; + + vars = push_var(vars, "XCE_JOBINDEX_X", "a"); + vars = push_var(vars, "xce_jobindex", "b"); + apply_env_vars(&vars, ARRAY_ELEMENT(2)); + + expect_env("XCE_JOBINDEX", "2"); + expect_env("XCE_JOBINDEX_X", "a"); + expect_env("xce_jobindex", "b"); + TEST_ASSERT_EQUAL_INT(0, count_warns()); + unsetenv("XCE_JOBINDEX_X"); + unsetenv("xce_jobindex"); +} + +/* 6.5: XCE_JOBID is the decimal job ID; a submission's own XCE_JOBID still + * overrides it, as before, with no WARN. */ +static void test_jobid_unchanged(void) +{ + env_var_t* vars = NULL; + char dec[32]; + + snprintf(dec, sizeof(dec), "%lu", (unsigned long)ARRAY_ELEMENT(4)); + apply_env_vars(&vars, ARRAY_ELEMENT(4)); + expect_env("XCE_JOBID", dec); + + snprintf(dec, sizeof(dec), "%lu", (unsigned long)ORDINARY_JOB); + apply_env_vars(&vars, ORDINARY_JOB); + expect_env("XCE_JOBID", dec); + + captured_count = 0; + vars = push_var(vars, "XCE_JOBID", "user-value"); + apply_env_vars(&vars, ORDINARY_JOB); + expect_env("XCE_JOBID", "user-value"); + TEST_ASSERT_EQUAL_INT(0, count_warns()); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_array_element_gets_its_number); + RUN_TEST(test_one_element_array_gets_one); + RUN_TEST(test_ordinary_job_gets_zero); + RUN_TEST(test_preupgrade_element_zero_gets_zero); + RUN_TEST(test_user_jobindex_ignored_with_warn); + RUN_TEST(test_user_jobindex_ignored_for_ordinary_job); + RUN_TEST(test_similar_names_still_applied); + RUN_TEST(test_jobid_unchanged); + return UNITY_END(); +}