admin-plane breakpoint 2: YAML resourcemap wiring + queue payload deep-copy
Breakpoint 2 of the admin-plane gaps (YAML flag/resourcemap wiring), plus
the queue payload ownership fix that the t1 controller segfault of
2026-10-23 12:41:13 traced to.
1. YAML resourcemap wiring (admin-plane-gaps 1.10):
- schema: Resource.distributions @8 / hostBased @9 uncommented --
resourcemap had no wire destination at all before this.
- resobj.c: resobj_init's #if 0 stub replaced with a real deep copy
(distributions + hbased_defined); resobj_destroy frees them. Deep
copy is mandatory: the source is myconf->cluster_config, which
keeps owning and later frees its tree.
- controller: reconcile_config_resources + seed_sharedinst_from_config
(cluster.c) seed c->sharedinst for SHARED|GLOBAL resources whose
distribution carries a value (the license-pool shape), from both
config_activate_committed paths, ahead of the conf4sched push.
- xceadmin: mapping.py maps resourcemap -> distributions (only the
location:[all]+value shape sets SHARED|GLOBAL), build.py renders
shared=true/distributions; golden expected.bin regenerated.
2. xcesched_push_configs resval double free (t1 12:39:52 abort): the
seeded push freed the resval entries AND the backing array via
free_name_val_list, then xlist_fini freed the same array again.
Detach xreslist before the FREEUP/xlist_fini block (same ownership
class bug-1506 fixed in sched_worker_change). Latent since inception:
every pre-1.10 push ran with nresvals==0 and never armed it.
3. queue payload deep-copy (t1 12:41:13 encode_Threshold_list segfault):
queue_init (create) MOVED the definition's users/hosts/thresholds/
window payloads out, and queue_refresh_def MOVED them out again --
but reconcile_config_queues runs MORE THAN ONCE over the same
cluster_config object (boot: pre-election activation AND phase2's
build_queues), so the second pass refreshed live queues from the
drained definition: thresholds became the poisoned (count>0,
array=NULL) pair, the generated encode_Threshold_list has no NULL
guard, and the next queue/worker push died on the NULL base.
Both paths now deep-copy (queue_init and queue_refresh_def); the
definition keeps its payloads, (NULL/0) genuinely means "no list in
this config", and reconcile is idempotent. New dup_group_config
helper in utils.c (the generated code has no GroupConfig dup).
Mirrors resobj_init's distributions deep-copy and worker.c
apply_join_state's dup pattern.
4. UT: test_reconcile_thresholds_survive_second_pass drives a triple
reconcile of a threshold-carrying queue and asserts definition
intactness plus a real encode_QueueConf -> wire -> decode round
trip; new test_config_resource_reconcile.c covers seed/deep-copy/
non-seed classes and idempotent re-seed.
Suites: controller_unit_test 1348/1348, sched_push 4, sched_worker_change 3,
apiserver_push 2, host_runusage 11, xcelib 759, xcealloc_oom 11,
99 scheduler binaries, all worker + harness suites, xceadmin pytest 375.
t1 E2E: boot double-reconcile clean, scheduler rejoin + full conf4sched
push applied, recovered crash-era PEND job reached DONE, 30-job saturation
flow (24 RUN + 6 PEND -> DONE), live gen-12 config activation with
re-push to the connected scheduler -- no crash, all four daemons healthy.
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
563f8e6890
commit
fc33a5da16
22 changed files with 1510 additions and 63 deletions
|
|
@ -182,6 +182,96 @@ def test_map_list_section_noop_on_empty_or_missing_items(cluster_config):
|
|||
assert len(cluster_config.resources) == 0
|
||||
|
||||
|
||||
def test_map_list_section_resourcemap_maps_distributions_and_all_flag(cluster_config):
|
||||
"""resourcemap -> distributions (admin-plane-gaps 1.10): one
|
||||
ResourceDistribution per resourcemap item in YAML order (location ->
|
||||
hosts, value -> the Value union member picked by YAML type). Only the
|
||||
location==["all"] + value shape sets XCE_RESF_SHARED|XCE_RESF_GLOBAL
|
||||
(the cluster-wide static shared pool / license-pool shape); other
|
||||
shapes map definition-only.
|
||||
"""
|
||||
items = [
|
||||
{
|
||||
"name": "licenses",
|
||||
"type": "numeric",
|
||||
"consumable": True,
|
||||
"resourcemap": [{"location": ["all"], "value": 100}],
|
||||
},
|
||||
{
|
||||
"name": "hostpool",
|
||||
"type": "numeric",
|
||||
"resourcemap": [
|
||||
{"value": 100, "location": ["host1", "host2"]},
|
||||
{"location": ["host5", "host6"]}, # dynamic, worker-reported
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "bigmem",
|
||||
"type": "boolean",
|
||||
"resourcemap": [{"value": True, "location": ["host1"]}],
|
||||
},
|
||||
{
|
||||
"name": "strres2",
|
||||
"type": "string",
|
||||
"resourcemap": [{"value": "v1", "location": ["host1"]}],
|
||||
},
|
||||
{"name": "plain", "type": "numeric"}, # no resourcemap at all
|
||||
]
|
||||
mapping.map_list_section(_spec_for("resources"), items, cluster_config)
|
||||
|
||||
r0, r1, r2, r3, r4 = cluster_config.resources
|
||||
|
||||
# [all]+value: SHARED|GLOBAL OR'd on top of CONSUMABLE
|
||||
assert r0.flags == 0x80 | 0x08 | 0x04 # CONSUMABLE | SHARED | GLOBAL
|
||||
assert len(r0.distributions) == 1
|
||||
assert [str(h) for h in r0.distributions[0].hosts] == ["all"]
|
||||
assert r0.distributions[0].value.u.which == "fvalue"
|
||||
assert r0.distributions[0].value.u.fvalue == 100.0
|
||||
|
||||
# host-subset static pool + dynamic shape: distributions mapped
|
||||
# faithfully, flags untouched (no SHARED|GLOBAL without location [all])
|
||||
assert r1.flags == 0
|
||||
assert len(r1.distributions) == 2
|
||||
assert [str(h) for h in r1.distributions[0].hosts] == ["host1", "host2"]
|
||||
assert r1.distributions[0].value.u.fvalue == 100.0
|
||||
assert [str(h) for h in r1.distributions[1].hosts] == ["host5", "host6"]
|
||||
assert r1.distributions[1].value.u.which == "ivalue32" # capnp union default
|
||||
|
||||
# boolean/string values pick their union member
|
||||
assert r2.distributions[0].value.u.which == "bvalue"
|
||||
assert bool(r2.distributions[0].value.u.bvalue) is True
|
||||
assert r3.distributions[0].value.u.which == "svalue"
|
||||
assert str(r3.distributions[0].value.u.svalue) == "v1"
|
||||
|
||||
# no resourcemap -> no distributions, no extra flags
|
||||
assert len(r4.distributions) == 0
|
||||
assert r4.flags == 0
|
||||
|
||||
|
||||
def test_map_list_section_resourcemap_real_repo_configs(cluster_config):
|
||||
"""The mapper handles the real src/configs/resources.yaml: every
|
||||
resourcemap entry lands as a distribution, and the two [all]+value
|
||||
resources (staticres4, res1) carry SHARED|GLOBAL.
|
||||
"""
|
||||
data = _load_real_yaml("resources.yaml")
|
||||
mapping.map_list_section(_spec_for("resources"), data["resources"], cluster_config)
|
||||
|
||||
by_name = {r.name: r for r in cluster_config.resources}
|
||||
for r in by_name.values():
|
||||
expected_maps = [item for item in data["resources"] if item["name"] == r.name][0].get("resourcemap") or []
|
||||
assert len(r.distributions) == len(expected_maps)
|
||||
|
||||
for name in ("staticres4", "res1"):
|
||||
assert by_name[name].flags & 0x08 | 0x04 == 0x08 | 0x04
|
||||
assert len(by_name[name].distributions) == 1
|
||||
assert [str(h) for h in by_name[name].distributions[0].hosts] == ["all"]
|
||||
assert by_name[name].distributions[0].value.u.fvalue == 100.0
|
||||
|
||||
# per-host/per-default shapes stay definition-only
|
||||
for name in ("staticres1", "staticres2", "staticres3", "host_quota"):
|
||||
assert by_name[name].flags & (0x08 | 0x04) == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# map_list_section: users.yaml/hosts.yaml -> ugroups/hgroups (named groups)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue