fix(scheduler): record each pend reason once per job (bug-1927)

The scheduler appended one NO_RESOURCE/MATCH_FAIL entry per host per
placement plan, so a pending parallel job carried plans x hosts copies
(~93k entries in the 1000-host bounds test). libxce gains
add_job_preason_once() with a per-context hash index keyed on
(host, resource, code) that keeps first-occurrence order; the
scheduler's three builders use it. add_job_preason() is unchanged for
other callers. The parjob equivalence test now expects each short host
once; the bounds test runs in ~2 s instead of ~19 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-09 06:30:58 +00:00
parent 6c3393aa86
commit 9f9b63dd24
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
10 changed files with 517 additions and 17 deletions

View file

@ -162,6 +162,183 @@ int add_job_preason(xce_job_preason_t* reasons, int code, char* host, char* res)
return rc;
}
/* ------------------------------------------------------------------------
* De-duplicating builder (bug-1927). Contract in preason.h. An open-
* addressing table (linear probing, load <= 1/2) of list positions keyed by
* the entry's hash; a hit is confirmed against the list entry itself, so
* the table never has to follow erases of the lists.
* ---------------------------------------------------------------------- */
#define PREASON_INDEX_MIN_SLOTS 64
static uint32_t preason_fnv1a(uint32_t h, const void* data, size_t n)
{
const unsigned char* p = (const unsigned char*)data;
size_t i;
for (i = 0; i < n; i++) {
h ^= p[i];
h *= 16777619u;
}
return h;
}
/* host is never NULL here ("" for a host-less resource reason) */
static uint32_t preason_key_hash(int is_res, int code, const char* host,
const char* res)
{
uint32_t h = 2166136261u;
h = preason_fnv1a(h, &is_res, sizeof(is_res));
h = preason_fnv1a(h, &code, sizeof(code));
h = preason_fnv1a(h, host, strlen(host) + 1);
if (is_res) {
h = preason_fnv1a(h, res, strlen(res) + 1);
}
return h;
}
/* Does `ref` name a list entry, still present, equal to the key? */
static int preason_ref_matches(const xce_job_preason_t* r, int ref, int is_res,
int code, const char* host, const char* res)
{
int pos = (ref - 1) >> 1;
if (((ref - 1) & 1) != is_res) {
return 0;
}
if (is_res) {
const xce_res_reason_t* e;
if (pos >= r->nres_reasons) {
return 0;
}
e = r->res_based_reasons[pos];
return (e != NULL) && (e->reason == code) && str_eq(e->host, host)
&& str_eq(e->resource, res);
} else {
const xce_host_reason_t* e;
if (pos >= r->nhost_reasons) {
return 0;
}
e = r->host_based_reasons[pos];
return (e != NULL) && (e->reason == code) && str_eq(e->host, host);
}
}
/* Doubles the table (first size PREASON_INDEX_MIN_SLOTS), rehashing every
* slot from its stored hash. 0 or -ENOMEM with the table unchanged. */
static int preason_index_grow(xce_preason_index_t* idx)
{
uint32_t* hashes;
int* refs;
int n;
int i;
if (idx->nslots > (1 << 29)) {
return -ENOMEM;
}
n = (idx->nslots == 0) ? PREASON_INDEX_MIN_SLOTS : idx->nslots * 2;
hashes = (uint32_t*)calloc((size_t)n, sizeof(*hashes));
refs = (int*)calloc((size_t)n, sizeof(*refs));
if ((hashes == NULL) || (refs == NULL)) {
FREEUP(hashes);
FREEUP(refs);
return -ENOMEM;
}
for (i = 0; i < idx->nslots; i++) {
int j;
if (idx->refs[i] == 0) {
continue;
}
for (j = (int)(idx->hashes[i] & (uint32_t)(n - 1)); refs[j] != 0;
j = (j + 1) & (n - 1)) {
}
hashes[j] = idx->hashes[i];
refs[j] = idx->refs[i];
}
FREEUP(idx->hashes);
FREEUP(idx->refs);
idx->hashes = hashes;
idx->refs = refs;
idx->nslots = n;
return 0;
}
int add_job_preason_once(xce_job_preason_t* reasons, xce_preason_index_t* idx,
int code, char* host, char* res)
{
const char* key_host;
uint32_t hash;
int is_res;
int mask;
int j;
int rc;
if ((reasons == NULL) || (idx == NULL)) {
return -EINVAL;
}
if ((host == NULL) && (res == NULL)) {
rc = add_job_preason(reasons, code, NULL, NULL);
return (rc < 0) ? rc : 0;
}
if ((2 * (idx->nused + 1)) > idx->nslots) {
rc = preason_index_grow(idx);
if (rc < 0) {
return rc;
}
}
is_res = (res != NULL);
key_host = (host != NULL) ? host : "";
hash = preason_key_hash(is_res, code, key_host, res);
mask = idx->nslots - 1;
for (j = (int)(hash & (uint32_t)mask); idx->refs[j] != 0;
j = (j + 1) & mask) {
if ((idx->hashes[j] == hash)
&& preason_ref_matches(reasons, idx->refs[j], is_res, code,
key_host, res)) {
return 0;
}
}
rc = add_job_preason(reasons, code, host, res);
if (rc <= 0) {
return (rc < 0) ? rc : -EINVAL;
}
/* rc is the list's new count: the entry sits at rc - 1 */
idx->hashes[j] = hash;
idx->refs[j] = 1 + (((rc - 1) << 1) | is_res);
idx->nused++;
return rc;
}
void erase_preason_index(xce_preason_index_t* idx)
{
if (idx == NULL) {
return;
}
FREEUP(idx->hashes);
FREEUP(idx->refs);
idx->nslots = 0;
idx->nused = 0;
}
xce_pendreason_t *make_pendreason(uint64_t jobid) {
xce_pendreason_t *reason = NULL;
xce_job_preason_t *preason = NULL;

View file

@ -1,3 +1,4 @@
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -518,6 +519,179 @@ static void test_preason_eq_does_not_modify_arguments(void)
erase_job_preason(&b);
}
/* ------------------------------------------------------------------------
* bug-1927: add_job_preason_once() keeps each (host, resource, code) and
* each (host, code) once, in first-add order.
* ---------------------------------------------------------------------- */
static void assert_res_entry(const xce_job_preason_t* r, int i, int code,
const char* host, const char* res)
{
TEST_ASSERT_TRUE(i < r->nres_reasons);
TEST_ASSERT_NOT_NULL(r->res_based_reasons[i]);
TEST_ASSERT_EQUAL_INT(code, r->res_based_reasons[i]->reason);
TEST_ASSERT_EQUAL_STRING(host, r->res_based_reasons[i]->host);
TEST_ASSERT_EQUAL_STRING(res, r->res_based_reasons[i]->resource);
}
static void test_once_identical_res_reason_kept_once(void)
{
xce_job_preason_t r;
xce_preason_index_t idx;
memset(&r, 0, sizeof(r));
memset(&idx, 0, sizeof(idx));
TEST_ASSERT_EQUAL_INT(1, add_job_preason_once(&r, &idx, 2, "h1", "slots"));
TEST_ASSERT_EQUAL_INT(0, add_job_preason_once(&r, &idx, 2, "h1", "slots"));
TEST_ASSERT_EQUAL_INT(0, add_job_preason_once(&r, &idx, 2, "h1", "slots"));
TEST_ASSERT_EQUAL_INT(1, r.nres_reasons);
TEST_ASSERT_EQUAL_INT(0, r.nhost_reasons);
assert_res_entry(&r, 0, 2, "h1", "slots");
erase_preason_index(&idx);
erase_job_preason(&r);
}
static void test_once_identical_host_reason_kept_once(void)
{
xce_job_preason_t r;
xce_preason_index_t idx;
memset(&r, 0, sizeof(r));
memset(&idx, 0, sizeof(idx));
TEST_ASSERT_EQUAL_INT(1, add_job_preason_once(&r, &idx, 3, "h1", NULL));
TEST_ASSERT_EQUAL_INT(0, add_job_preason_once(&r, &idx, 3, "h1", NULL));
TEST_ASSERT_EQUAL_INT(1, r.nhost_reasons);
TEST_ASSERT_EQUAL_INT(0, r.nres_reasons);
TEST_ASSERT_EQUAL_INT(3, r.host_based_reasons[0]->reason);
TEST_ASSERT_EQUAL_STRING("h1", r.host_based_reasons[0]->host);
erase_preason_index(&idx);
erase_job_preason(&r);
}
/* Entries differing in any one of code, host or resource are distinct; a
* host reason and a resource reason on the same host are distinct; a
* host-less (shared) resource reason is kept once too. First-add order. */
static void test_once_distinct_entries_kept_in_first_add_order(void)
{
xce_job_preason_t r;
xce_preason_index_t idx;
memset(&r, 0, sizeof(r));
memset(&idx, 0, sizeof(idx));
TEST_ASSERT_EQUAL_INT(1, add_job_preason_once(&r, &idx, 2, "h1", "slots"));
TEST_ASSERT_EQUAL_INT(2, add_job_preason_once(&r, &idx, 2, "h1", "mem"));
TEST_ASSERT_EQUAL_INT(3, add_job_preason_once(&r, &idx, 2, "h2", "slots"));
TEST_ASSERT_EQUAL_INT(4, add_job_preason_once(&r, &idx, 1, "h1", "slots"));
TEST_ASSERT_EQUAL_INT(5, add_job_preason_once(&r, &idx, 2, NULL, "lic"));
TEST_ASSERT_EQUAL_INT(1, add_job_preason_once(&r, &idx, 2, "h1", NULL));
TEST_ASSERT_EQUAL_INT(2, add_job_preason_once(&r, &idx, 3, "h1", NULL));
/* every one again, in another order: nothing appended */
TEST_ASSERT_EQUAL_INT(0, add_job_preason_once(&r, &idx, 3, "h1", NULL));
TEST_ASSERT_EQUAL_INT(0, add_job_preason_once(&r, &idx, 2, NULL, "lic"));
TEST_ASSERT_EQUAL_INT(0, add_job_preason_once(&r, &idx, 2, "h2", "slots"));
TEST_ASSERT_EQUAL_INT(0, add_job_preason_once(&r, &idx, 1, "h1", "slots"));
TEST_ASSERT_EQUAL_INT(0, add_job_preason_once(&r, &idx, 2, "h1", "mem"));
TEST_ASSERT_EQUAL_INT(0, add_job_preason_once(&r, &idx, 2, "h1", NULL));
TEST_ASSERT_EQUAL_INT(0, add_job_preason_once(&r, &idx, 2, "h1", "slots"));
TEST_ASSERT_EQUAL_INT(5, r.nres_reasons);
assert_res_entry(&r, 0, 2, "h1", "slots");
assert_res_entry(&r, 1, 2, "h1", "mem");
assert_res_entry(&r, 2, 2, "h2", "slots");
assert_res_entry(&r, 3, 1, "h1", "slots");
assert_res_entry(&r, 4, 2, "", "lic"); /* host-less: stored as "" */
TEST_ASSERT_EQUAL_INT(2, r.nhost_reasons);
TEST_ASSERT_EQUAL_INT(2, r.host_based_reasons[0]->reason);
TEST_ASSERT_EQUAL_INT(3, r.host_based_reasons[1]->reason);
erase_preason_index(&idx);
erase_job_preason(&r);
}
/* A main reason erases the lists; the index must not then claim the
* erased entries are still present. */
static void test_once_after_main_reason_erase_adds_again(void)
{
xce_job_preason_t r;
xce_preason_index_t idx;
memset(&r, 0, sizeof(r));
memset(&idx, 0, sizeof(idx));
TEST_ASSERT_EQUAL_INT(1, add_job_preason_once(&r, &idx, 2, "h1", "slots"));
TEST_ASSERT_EQUAL_INT(1, add_job_preason_once(&r, &idx, 3, "h2", NULL));
TEST_ASSERT_EQUAL_INT(0, add_job_preason_once(&r, &idx, 1, NULL, NULL));
TEST_ASSERT_EQUAL_INT(1, r.main_reason);
TEST_ASSERT_EQUAL_INT(0, r.nres_reasons);
TEST_ASSERT_EQUAL_INT(0, r.nhost_reasons);
TEST_ASSERT_EQUAL_INT(1, add_job_preason_once(&r, &idx, 3, "h2", NULL));
TEST_ASSERT_EQUAL_INT(1, add_job_preason_once(&r, &idx, 2, "h9", "mem"));
TEST_ASSERT_EQUAL_INT(2, add_job_preason_once(&r, &idx, 2, "h1", "slots"));
TEST_ASSERT_EQUAL_INT(0, add_job_preason_once(&r, &idx, 2, "h1", "slots"));
TEST_ASSERT_EQUAL_INT(2, r.nres_reasons);
assert_res_entry(&r, 0, 2, "h9", "mem");
assert_res_entry(&r, 1, 2, "h1", "slots");
TEST_ASSERT_EQUAL_INT(1, r.nhost_reasons);
erase_preason_index(&idx);
erase_job_preason(&r);
}
/* The bounds-test shape at scale: 64 passes over 2000 hosts keep 2000
* entries (the index grows past its first size). */
static void test_once_many_passes_keep_one_entry_per_host(void)
{
xce_job_preason_t r;
xce_preason_index_t idx;
char host[16];
int pass;
int i;
memset(&r, 0, sizeof(r));
memset(&idx, 0, sizeof(idx));
for (pass = 0; pass < 64; pass++) {
for (i = 0; i < 2000; i++) {
snprintf(host, sizeof(host), "bh%04d", i);
TEST_ASSERT_EQUAL_INT((pass == 0) ? (i + 1) : 0,
add_job_preason_once(&r, &idx, 2, host,
"slots"));
}
}
TEST_ASSERT_EQUAL_INT(2000, r.nres_reasons);
for (i = 0; i < 2000; i++) {
snprintf(host, sizeof(host), "bh%04d", i);
assert_res_entry(&r, i, 2, host, "slots");
}
erase_preason_index(&idx);
erase_job_preason(&r);
}
static void test_once_bad_arguments(void)
{
xce_job_preason_t r;
xce_preason_index_t idx;
memset(&r, 0, sizeof(r));
memset(&idx, 0, sizeof(idx));
TEST_ASSERT_EQUAL_INT(-EINVAL, add_job_preason_once(NULL, &idx, 2, "h1",
"slots"));
TEST_ASSERT_EQUAL_INT(-EINVAL, add_job_preason_once(&r, NULL, 2, "h1",
"slots"));
TEST_ASSERT_EQUAL_INT(0, r.nres_reasons);
erase_preason_index(NULL);
erase_preason_index(&idx); /* never used: nothing to free */
}
void test_preason(void)
{
printf("\n>> preason test\n\n");
@ -542,4 +716,10 @@ void test_preason(void)
RUN_TEST(test_preason_eq_null_list_entries);
RUN_TEST(test_preason_eq_null_string_fields);
RUN_TEST(test_preason_eq_does_not_modify_arguments);
RUN_TEST(test_once_identical_res_reason_kept_once);
RUN_TEST(test_once_identical_host_reason_kept_once);
RUN_TEST(test_once_distinct_entries_kept_in_first_add_order);
RUN_TEST(test_once_after_main_reason_erase_adds_again);
RUN_TEST(test_once_many_passes_keep_one_entry_per_host);
RUN_TEST(test_once_bad_arguments);
}