Commit graph

2504 commits

Author SHA1 Message Date
lei.chen
9d7628ef3f worker: re-report job state on transparent failover reconnect
Covers the case where the worker process stays alive and only the TCP link
to the controller drops (or the controller fails over): libxce's failover
loop reconnects transparently and xcecli_hello() returns 0, so
checking_connection() sees ret==0 and never takes its
xcecd_join_cluster()/resend_job_status() path — job state changes that
occurred while disconnected are never re-reported, leaving stale RUN/UNKWN
jobs on the controller.

- worker/controller_connector.c: new xcecd_reconnect_cb registered via
  xcecli_set_reconnect_handler after a successful join, resending the actual
  job state (bug-1509 semantics) on every transparent reconnect.
- lib/client.c (bug-1522): xcecli_start no longer resets rejoin_cb/rejoin_arg
  to NULL — it is initialized by xcecli_ctx_init's calloc, and a pre-start
  registration (scheduler sched_reconnect_cb, worker xcecd_reconnect_cb) must
  survive first connect; the failover loop invokes it after a transparent
  reconnect (xce_ctx_reconnect), which never re-runs xcecli_start. Previously
  every pre-start registration was dead code.
- scheduler.c: move xcecli_set_reconnect_handler to AFTER a successful join —
  xcecli_join also runs the failover loop, and a candidate-iteration rotate
  (standby redirect) must not fire the rejoin hook before the session exists
  (was firing sched_reconnect_cb during initial join → rejoin on
  uninitialized state → invalid-opcode crash).
- test_libxce_retry.c: regression test for bug-1522
  (test_reconnect_handler_survives_start_state_reset). Suite 296/296.

Functional verification on the test cluster: worker joins (host OK), runs a
job, and after a controller-link break re-joins ("Rejoined cluster
successfully after retry N"). Scheduler stays up (no invalid-opcode crash).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-15 08:40:20 +00:00
lei.chen
ee791791bd scheduler: time-window dispatch reason (PEND-reason #17/E11)
Add queue dispatch-window support so a queue whose window is closed
(queues.yaml dispatch_window, e.g. 19:00-08:30) is treated as
non-electable and reports PREASON_TIME_WINDOW.

- lib/window.c: new xce_timewindow_is_open() — NULL/empty always open,
  same-day and overnight (close < open) segments, day-scoped (0=every day,
  1-7=Mon..Sun).
- scheduler: make_sched_queue duplicates dispatchwindow_config onto
  xce_sched_queue_t.dispatchwindow (freed by kill_sched_queue);
  sched_queue_is_ready() checks it; sched_queue_status_reason() now takes
  the queue (was a bare status bitmask) and maps a closed window to
  PREASON_TIME_WINDOW.
- fcfs: keep the windowed-out queue skip; do NOT attach the queue-status
  reason to job->schedobj from the picker thread (bug-1521: races the
  allocator thread's MOVE_PTR(obj->reason,...) and corrupts the heap).
- tests: test_window.c (5 cases) covers no-window/daily/overnight/
  day-scoped/multi-seg; full suite 295/295 pass.

Cluster-verified: with a 09:00-17:00 window on the xce queue (currently
outside the window), fcfs reports "queue xce ... ready=0 reason=8"
(PREASON_TIME_WINDOW).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-15 06:50:51 +00:00
lei.chen
53c0083410 fix(schedulerclient): parse numeric job_status + object-valued pending_reason
Two bugs surfaced by the new scheduler_pend_reasons tool:
1. The apiserver returns job_status as the numeric XCE state bitmask
   (PEND=0x01, RUN=0x02, EXIT=0x04, DONE=0x08), but getJobs read it with
   rowStr (string), which returns "" for a number → every job mapped to URM
   "unknown", so the pending/running status filter matched nothing.
   → JobStatusFromXCEState(int) + rowURMStatus fallback.
2. pending_reason is a JSON object in the row, not a string; rowStr returned
   "" so Extensions["pending_reason"] was empty and the reason never parsed.
   → rowJSON(key) stores the raw JSON value; the MCP pend_reasons parser
   handles the map form.

schedulerclient tests updated (JobStatusFromXCEState table). mcp-server +
schedulerclient test suites pass; live MCP call now returns the bjobs -p style
explanation for a PEND job.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-15 03:35:22 +00:00
lei.chen
67abbfa492 feat(mcp-server): scheduler_pend_reasons tool — bjobs -p equivalent
New read-only tool that answers "why is my job waiting": lists PEND jobs (or
one by id) and translates each job's pending_reason into a human-readable
scenario (LSF bjobs -p style). Maps the PREASON_* codes surfaced by the
PEND-reason work (4e2ac86): no_resource / match_fail / limit_global /
limit_host / queue_closed / queue_draining / time_window / host_unavail.

- internal/pendreason.go: pending_reason JSON parsing (mirrors apiserver
  rawPendReason) + reasonScenario() translation + bjobs -p style summary.
- internal/app.go: HandlePendReasons — id param for one job, else pending
  list filtered by queue/owner/limit.
- cmd/main.go: register scheduler_pend_reasons (read-only).
- 5 unit tests (known/unknown codes, parse from JSON, missing reason,
  summary format). go test ./... + go vet clean.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-15 03:25:28 +00:00
lei.chen
0e9609af6d docs: record PEND reason P0+P1 implementation status (4e2ac86) 2026-08-14 09:43:44 +00:00
lei.chen
4e2ac861f4 feat(scheduler): PEND reason classification — main_reason + limit/queue/worker reasons (POC "why is my job waiting")
P0 (done, cluster-verified):
- main_reason was always 0: add_job_preason only set it when BOTH host and res
  were NULL, but the allocator always attaches host/res detail rows. Now the
  first detail add wins as main_reason, so the API's main_reason_name is
  meaningful (was "unknown(0)"). Verified live: PEND jobs now report
  main_reason=3/match_fail + per-host reason_name.
- limit plugin attach: host-based quota hit (hg1_slots_per_host) -> LIMIT_HOST
  + limit name; global quota hit (ug1_slots, ...) -> LIMIT_GLOBAL + limit name.
  Previously both blocks were silent (job just stayed PEND).
- Extended the PREASON_* enum (was 3 codes) with LSF-style classes:
  LIMIT_GLOBAL/LIMIT_HOST/QUEUE_CLOSED/QUEUE_DRAINING/TIME_WINDOW/HOST_UNAVAIL.
  apiserver pendReasonName maps them; host/res detail rows now carry reason_name.

P1 (implemented):
- queue status: sched_queue_is_ready no longer a stub — closed/inactivated
  queues are not electable; the fcfs election attaches QUEUE_CLOSED /
  QUEUE_DRAINING to every PEND job in a skipped queue.
- worker filter: sched_try_alloc_on_host_ skips UNAVAIL/CLOSED workers and
  attaches HOST_UNAVAIL instead of trying to allocate on them.

Unit tests: preason suite updated for the main_reason change + 3 new tests
(limit host/global, first-add-wins); 295 lib tests pass under ASAN.
apiserver backend tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 09:43:21 +00:00
lei.chen
62778930e2 docs: mark apiserver auto-rejoin note as fixed (e505066) — task #12 done 2026-08-14 08:05:20 +00:00
lei.chen
e505066162 fix(apiserver): auto-recover after full controller death+restart (task #12)
The apiserver stayed circuit-breaker-degraded after a full controller
restart until manually restarted. Two root causes:

1. Breaker recovery was decoupled from the breaker itself. The breaker can
   be tripped by a backend write failure (e.g. E_XCE_STANDBY -10014 while
   the controller is electing) with no heartbeat ever failing, but
   StartHeartbeat only recovered on a fail→success heartbeat transition
   (prevFailed), so the breaker stayed degraded forever.
   → extract needsRejoinAfterHeartbeat(hbOK, prevFailed): recover when
   prevFailed OR the breaker is currently tripped. 5 new unit tests.

2. The 30s heartbeat's fire-and-forget reconnect only fires when the lib's
   internal ctx state is XCE_CTX_ERR. After a controller restart whose
   election outlasts the FAILOVER_CALL retry budget, the retry loop's
   xcecli_ctx_recover leaves the ctx RUN on a half-open connection, so the
   FF path never reconnects and the client stays disconnected forever.
   → lib: new xcecli_force_reconnect() forces a rotate+recover regardless
   of state; apiserver: 2s recoveryLoop force-reconnects + rejoins while
   degraded.

Verified on cluster: after controller kill+restart, the apiserver logs
"backend re-joined controller after reconnect" and job submissions resume
without a manual restart; a fresh job runs to DONE. Controller, scheduler
(rejoin ok, bug-1513) and workers all auto-recover.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 08:01:35 +00:00
lei.chen
7f8c07ba85 fix(scheduler): bug-1513 — rejoin crash root-caused to 3 rejoin re-push bugs, not a lib race
The "libxce reconnect heap corruption" (ASAN bad-free, free(): invalid
pointer) was NOT a libxce bug — xce_ctx_reconnect is fine. The crashes are
in the scheduler's rejoin state re-sync path (config/dataset re-push),
fixed with the reconnect/rejoin feature now integrated:

1. group.c makeGroup refresh: FREEUP(m->u.name) raw-freed shared interned
   xstrings (makeString is a global refcounted table); the correct release
   is dereference(), matching group_killer. → dereference().
2. limit.c limit_update_config: after key_add returns -EEXIST (rejoin
   re-push — limit name already present) it called kill_limit(limit), but
   xkvmgr_add's vclear already freed the limit via the kvmgr datakiller
   (=kill_limit). → skip on -EEXIST (idempotent re-push).
3. handler.c/scheduler.c: the config push was applied inline on the vcpu
   thread (sched_init_config adds queues/plugin queues) while job updates
   flow through pend_updates on the main loop — concurrent rejoin re-push
   let fcfs_update_job see a NULL j->queue (SIGSEGV). Route the config
   push through pend_updates (XCE_SCHED_OP_UPDATE_CONFIG, killer case
   already existed) and process config requests FIRST in sched_update via
   a staging queue so jobs always see their queues/resources.

Verified: 295 lib unit tests pass under ASAN (incl. 2 new group-refresh
regression tests), scheduler survives 3 consecutive controller restarts
(each rejoin ok), full job lifecycle (submit→dispatch→DONE) on the
cluster. Also fixed stale build deps (libxce_deps missing librtti_dep,
scheduler_deps missing ssl_dep/c_capnproto_dep, unity include path).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 07:29:32 +00:00
lei.chen
2044dedb2f docs: task 12 — apiserver circuit breaker done (2bbe894), recovery gap noted 2026-08-14 05:21:07 +00:00
lei.chen
2bbe894f99 feat(apiserver): fail-fast circuit breaker when backend controller is down (Req 4)
component-failover-adoption Req 4: when the apiserver's controller connection
is down, backend-requiring API requests must return a clear service-unavailable
error rather than hanging. Adds:

- health.go: an atomic circuit breaker (IsDegraded/MarkDegraded/MarkHealthy).
- callBackend: the four write calls (jobsubmit/jobctrl/queuectrl/hostctrl)
  now route through it — degraded ⇒ immediate "backend controller unavailable"
  error without invoking the lib; a transport OR XCE not-ready/masterless/
  standby failure (E_XCE_NOMASTER/REDIRECT/NOT_READY/STANDBY) trips the breaker
  so the NEXT call fails fast. Semantic rejections (-EINVAL/-ENOENT/E_XCE_BAD_*)
  never trip it.
- StartHeartbeat: a failed tick marks degraded; recovery re-joins (rejoinBackend)
  and marks healthy.

Verified live (t1 cluster): kill controller → first submit returns
xcecli_jobsubmit failed: -10013 (bounded, no hang), second submit returns
"backend controller unavailable (reconnecting)" in ~11ms. 7 unit tests.
Recovery-via-heartbeat after a FULL controller death is NOT yet reliable — the
lib transport reconnect does not always re-establish to the restarted
controller (same class as the scheduler reconnect; tracked under task 13).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 05:20:52 +00:00
lei.chen
d4b98aab01 docs: scheduler reconnect — confirmed real heap corruption (ASAN + debug both crash)
Deep-dive: the rejoin works but crashes ~3-8s later with a REAL heap corruption
(ASAN bad-free freeing a pointer 56B into a 60B region, all-loader frames;
non-ASAN debug build aborts with free(): invalid pointer, gdb stack corrupted).
The endpoint-unmonitor fix (correct, matches xce_ctx_stop) did not help.
Candidate roots: config re-push re-running sched_init_config non-idempotently
(sharedInst MOVE_PTR/zarray_put), lib reconnect vs in-flight vtask, or
state-resync merge. All scheduler reconnect changes reverted to keep the
scheduler stable; this is a dedicated deep-debugging task.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 03:09:04 +00:00
lei.chen
f3e510a548 test(scheduler): G-5 JOB_RUSAGE dequeue routing — routes to sched_apply_job_rusage
Closes the G-5 test gap mirroring the G-8 routing test: the rusage notify
handler enqueues XCE_SCHED_OP_JOB_RUSAGE and sched_update() must dispatch it to
sched_apply_job_rusage(), storing the report on the matching job (bug-765 G-5
remediation, production fix already in 93124c5). Driving the real sched_update()
dequeue loop with a recording stub, asserts the request reaches
sched_apply_job_rusage and the job's rusages field is set.

11/11 sched_redispatch_test PASS; full scheduler suite 6/6 OK. Live: job ran to
DONE on the deployed scheduler (G-3 leak fix in place), zero ASAN errors.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 02:52:09 +00:00
lei.chen
5d345d86b5 docs: failover-adoption — worker+apiserver done (2165b1a), scheduler blocked by lib race 2026-08-14 02:35:34 +00:00
lei.chen
2165b1ab12 feat(failover): worker keeps retrying forever + apiserver re-joins on reconnect
component-failover-adoption Req 2 (partial): two of the three daemons now
self-heal after a controller restart without an operator restart.

- worker (controller_connector.c): checking_connection() previously gave up
  after 5 retries and called handle_disconnect() (which set running=false and
  exited the connector thread). It now retries indefinitely — the connector
  thread is dedicated to this loop so the unbounded retry/sleep cannot block
  the worker's other liveness duties. Verified live: killing the controller
  leaves all 3 workers alive and retrying; they re-join when it returns.

- apiserver (clib.go): xcecli_heartbeat is fire-and-forget — it best-effort
  reconnects the transport but never re-joins, so after a controller restart
  push notifications stop. StartHeartbeat now tracks heartbeat failure→success
  and, on recovery, calls xce_backend_rejoin (xcecli_join as HostRole_apiserver)
  to re-register and resume pushes. Verified live: "backend re-joined
  controller after reconnect" logged during a controller kill/restart.

Scheduler reconnect NOT included: the rejoin works but exposes a pre-existing
libxce race — xce_ctx_reconnect's endpoint_fini tears down the old endpoint
while a lib vtask/io thread is still processing it, a deterministic double-free
(bad-free in the loader, same freed address offset across runs). Root-cause in
libxce netio/endpoint lifecycle is a separate task (documented in
left_scheduler_work.md).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 02:35:05 +00:00
lei.chen
200303d8fe test(scheduler): G-8 dequeue routing — UPDATE_JOB_FIELDS → in-place, bulk → no damage
Closes the msg-gap G-8 test gap the research flagged ("handler→dequeue ROUTING
is verified by code review, NOT an automated integration test"). The
remediation (UPDATE_JOB_FIELDS op routed to sched_update_existing_job) is
already in the code (93124c5); these tests lock the routing:

- test_update_fields_routes_to_inplace_path: driving the real sched_update()
  dequeue loop, an UPDATE_JOB_FIELDS request reaches sched_update_existing_job
  and mutates the existing job in place (same object, runtime state kept).
- test_bulk_update_existing_job_no_damage: a re-pushed dataset row for an
  existing jobid is EEXIST-dropped by key_add (via the real sched_add_job) —
  the live job keeps identity/state/runtime state.

10/10 sched_redispatch_test PASS; full scheduler suite 6/6 OK. Live cluster:
job PEND→RUN→DONE via notify updates, no WARN, no duplicate instance.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 02:08:49 +00:00
lei.chen
6f520a677b docs: record PEND reason classification as unfinished work (pending unified analysis)
Adds the PEND reason taxonomy (docs/pend-reason-investigation-design.md) to
left_scheduler_work 八.4 推迟/未决, with the three open decisions for unified
analysis: code strategy (reuse 3 codes + subcodes vs expand PREASON_* to ~20),
wiring priority (P0 main_reason + limit attach), and bjobs -p-equivalent MCP
capability.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 01:12:14 +00:00
0cb5918517
chore(wolf): checkpoint buglog.json (897 bugs) 2026-08-13 13:19:20 +00:00
922d57bb4f
chore(wolf): checkpoint buglog.json (895 bugs) 2026-08-13 12:01:04 +00:00
369ee9fefc
chore(wolf): checkpoint buglog.json (894 bugs) 2026-08-13 10:53:35 +00:00
558c92d66d
chore(wolf): checkpoint buglog.json (893 bugs) 2026-08-13 10:48:23 +00:00
726bcc52f5
chore(wolf): checkpoint buglog.json (893 bugs) 2026-08-13 10:46:23 +00:00
lei.chen
f66a626bae docs: PEND reason investigation + classification design (LSF-aligned)
Survey of every mechanism that can block a job from dispatch across the
scheduler (election → allocation → commit): 18 blocking points in 6
categories. Only 3 produce a PEND reason (MATCH_FAIL + 2×NO_RESOURCE);
the limit plugin (4 configured limits), queue state, dispatch_window and
fairshare are configured but silent; 8 LSF-standard reason categories
(dependency/license/reservation/exclusive/worker-state/...) are missing or
stubbed. Includes a layered reason taxonomy and wiring priorities.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-13 10:24:32 +00:00
53dfd078c9
chore(wolf): checkpoint buglog.json (892 bugs) 2026-08-13 09:31:50 +00:00
lei.chen
86265fb7bc docs: mark PEND reason end-to-end done (bug-1513), note main_reason follow-up
- aibot_poc_investiagting.md: PEND reason 调查完成 + 转换层补全;新增遗留项(main_reason 恒 0)
- buglog.json: bug-1513 entry
- memory.md: session log

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-13 09:16:49 +00:00
lei.chen
ac6131dbd7 feat(apiserver): expose PEND reason end-to-end (scheduler → API)
The pending_reason field was declared on models.JobInfo but never populated:
scheduler computed PEND reasons and the controller stored them on
job->pend_reasons, yet the data stopped there. Two gaps closed:

1. controller/apiserver.c: apiserver_job_hook_fun now handles
   HOOK_DETAIL_SETPREASON by pushing the updated job to API servers (reusing
   apiserver_job_update_fun). Without this, a job whose PEND reasons change
   never reached the API server — only JOB_NEW/UPDATE/START/EXIT events did,
   and setpreason fired none of those.

2. backend/convert.go + convert_pure.go: cJobToModel / rawJobToModel now
   extract xce_job_t.pend_reasons (main_reason + host/res reasons) and
   marshal them into models.JobInfo.PendingReason as JSONB. models/jobs.go:
   pending_reason added to JOB_DEFAULT_COLUMN so default job queries return
   it (it was only in JSONBJFields, never selected).

Verified live on t1/t2/t3: a job with resreq "type == nosuchhost" stays
PEND and returns
  "pending_reason":{"main_reason":0,"host_reasons":[{"host":"t1","reason":3},
   {"host":"t2","reason":3},{"host":"t3","reason":3}]}
(main_reason=3 PREASON_MATCH_FAIL per host) via both /jobs list, /jobs/:id
and the SSE stream. 4 new unit tests (rawJobToModel pendReason marshal +
name mapping), all apiserver backend tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-13 09:16:13 +00:00
ab6639df56
chore(wolf): checkpoint buglog.json (885 bugs) 2026-08-13 08:27:28 +00:00
5bf123cc20
refactor(controller,xceadmin): drop generic Resource.unit, hardcode bytes for maxmem
Simplification per review feedback on e3b3ffa: a generic schema-driven
unit field touched the capnp wire schema (2 structs), C structs
(xce_resource_t/xce_resval_t), resobj.c's deep-copy/destroy, opget.c's
reply-build lookup, resources.yaml/resources.schema.json, and mapping.py
-- real cross-cutting cost for a mechanism only one resource (maxmem)
actually needs today.

Reverted all of that. The controller CLI (test.c) now just knows
"maxmem" is bytes-valued via a small explicit is_bytes_valued_resource()
name list, and adaptively picks the best-fit human unit at display time
(format_bytes_human, unchanged) -- same observable behavior (resval/
workerlist render maxmem as e.g. "30.90 GB"), zero wire schema surface.
Regenerated the xceadmin golden fixture back to its pre-unit-field size
(5304 bytes, exactly matching the size right after the mxj-only fix,
confirming a clean, complete removal).

Verified: 1095/1095 controller_unit_test (3 runs; one hit the
pre-existing flaky segfault already logged as bug-1553, unrelated),
228/228 xceadmin pytest, clean rebuild of lib/controller/xceadmin/xce_test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 08:27:16 +00:00
e3b3ffaa97
fix(controller,xceadmin): fix zero-slot scheduling block, misleading resval logs, byte-unit display
Three bugs found while diagnosing a live controller log, all in the
worker resource-value path:

1. Every host relying on the documented hosts.yaml auto-detect default
   (no max_slot, meaning "use CPU count") got 0 scheduling slots,
   permanently blocking the scheduler from placing jobs on it. mxj is
   UInt32 on the wire and can't carry the schema's -1 sentinel; mapping.py
   worked around this by leaving it unset, which defaults to 0 -- and 0 is
   never < 0, so apply_join_state()'s auto-detect fallback never fired.
   Fixed by encoding the sentinel as 0xFFFFFFFF, which the controller's
   `int mxj` field reinterprets back to -1 via its existing implicit
   uint32_t->int conversion. No C-side or wire schema change needed for
   this part; the fallback itself was already correct but had zero test
   coverage for a negative mxj, now added.

2. update_resvals()/update_resval_list() logged the failure reason via
   PERROR, which reads process-global errno -- but update_resval()'s
   failure paths return raw negative codes directly and never touch
   errno, so the controller log filled with misleading "update_resval:
   Success" / "update_resval: Resource temporarily unavailable" lines
   instead of the real reason. Fixed to decode the actual rc via
   xce_strerror_r and name the failing resource.

3. maxmem is canonically stored/wired as raw bytes but had no unit
   metadata, so resval/workerlist printed it as a bare number that read
   as garbage (e.g. workerlist's fixed-width columns visually smeared a
   multi-GB byte count across the MaxCPU/MaxMem headers). Added an
   optional `unit` field to the Resource/ResourceVal capnp structs (and
   xce_resource_t/xce_resval_t/resources.yaml) so resval and workerlist
   can render a human-readable value (e.g. "30.90 GB") while the stored
   value stays raw bytes -- display-only, every other resource unaffected.

Regenerated the xceadmin golden fixture (fixtures/full/expected.bin)
twice: once for the mxj sentinel encoding change, once for the new
Resource.unit field growing every resource's wire size by one pointer
slot. Both diffs verified byte-for-byte isolated to exactly the expected
change before regenerating.

Verified: 1095/1095 controller_unit_test (stable across repeated runs),
229/229 xceadmin pytest, clean rebuild of lib/controller/xceadmin/xce_test.
Two related findings logged to .wolf/buglog.json but deliberately not
fixed here (out of scope per discussion): a pre-existing flaky segfault
in the combined controller_unit_test binary, and maxswap/maxtmp being
hardcoded stub values on the worker side.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 08:12:09 +00:00
9e15161228
chore(wolf): checkpoint buglog.json (880 bugs) 2026-08-13 08:10:28 +00:00
lei.chen
7b7294dd1e docs: record bug-1512 multi-worker over-dispatch fix + wolf bookkeeping
- left_scheduler_work.md: bug-1494 子问题三(bug-1512)已修复,多 worker 8/8/8 均匀分布验证
- buglog.json: bug-1512 entry(controller code=0 成功 ack 被误判为失败)
- cerebrum.md: Do-Not-Repeat — notify 带 outcome code 字段时需按 controller 侧实际值做 gate
- memory.md: session log

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-13 06:22:00 +00:00
lei.chen
e4aaef4b3b fix(scheduler): re-PEND only on real dispatch failures, not the success ack
The controller acknowledges EVERY dispatched job through
NotifyData_data_jobDispatchFail (controller/job.c handle_jobdispatch):
a negative code is a real rejection, code 0 is the success ack (the else
branch calls notify_dispatch(jobid, 0)). sched_handle_notify treated any
jobDispatchFail as a failure and returned the job to PEND, so every
successfully dispatched job was immediately re-PENDed, re-elected, and
re-dispatched while the original instance kept running — spawning
duplicate instances cluster-wide. On a 3-worker cluster 24 submitted jobs
ran 38 instances and piled onto the first worker (t1) because its slots
were counted for each duplicate.

Fix: only sched_requeue_pend_job() when fail->code < 0; consume the
success ack (code 0) as a no-op — the job is already RUN
(sched_commit_decision marks it at commit). This was a regression of the
bug-1494 re-PEND logic (bug-1510 fix) that lacked the code guard.

Verified live on t1/t2/t3: 24x sleep-300 jobs distributed exactly 8/8/8,
zero duplicates, all DONE. Add white-box test
test_handle_dispatch_fail_requeues_only_on_negative_code.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-13 06:20:43 +00:00
ff87986950
chore(wolf): checkpoint buglog.json (871 bugs) 2026-08-13 04:28:57 +00:00
ba4e5fd227
chore(wolf): guard buglog.json integrity, add dedupe rule and auto-checkpoint
Recovered .wolf/buglog.json (828 entries lost when this worktree's copy
silently reset to bug-001 sometime after commit ce96fd6d, 3 days
uncommitted with zero overlap detection) by merging the committed
history back in and renumbering the 43 real entries logged since.

To stop this recurring:
- OPENWOLF.md: require searching for an existing same-file/root_cause
  entry before appending a new bug id (fixes the bug-1418 x4 duplication
  pattern found in the recovered history); never restart the id counter.
- stop.js: validate buglog.json every turn (JSON validity, {version,bugs}
  wrapper, duplicate ids, count/max-id not regressing vs. the last
  commit) and auto-commit it standalone when it grew cleanly, so git
  history is always a recoverable backup instead of a 3-day-stale one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 04:28:44 +00:00
d170623bd0
docs(component-failover-adoption): mark task 5.4 complete, close spec
Final regression sweep (task 5.4) confirms zero new regressions across
the full stack (lib/controller/scheduler/worker/xceadmin/apiserver/xcecli)
and proves the single-candidate path is identical to the multi-candidate
path across all three components (Req 1.5), closing out all 21 tasks
(1.1-5.4) of component-failover-adoption.

Two production bugs found during live E2E validation (5.1/5.2) remain
open and out of this spec's scope: bug-039 (apiserver idle-period rejoin
structurally unobservable on a real single-election failover) and
bug-040 (job pushes not reaching apiserver's local store, likely
downstream of bug-039).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 03:38:40 +00:00
lei.chen
8871f7e204 docs: update status — Phase 2 done, human jobid, priorities (2026-08-13)
- left_scheduler_work.md: Phase 2 implemented (SSE+WatchJobs+4 tools), add
  job_id_human fix + ListJobs filter fix + .mcp.json env-var note; refresh
  priority list (bug-1490/1494/Phase2 all done).
- Scheduler-Agent-设计文档.md: v1.3, status header reflects Phase 0-2 done +
  verified.
- cerebrum: user preference (apiserver owns data correctness, MCP is a thin
  reader); jobid dual-encoding lesson (reuse backend.FormatJobID).
- memory: session log.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-13 03:35:00 +00:00
51589b68eb
docs(component-failover-adoption): mark task 5.3 complete, circuit breaker validated live
Live E2E of apiserver's backend-health circuit breaker (task 4.5):
killed all 3 controller nodes (not just the primary, unlike 5.1/5.2 --
this scenario needs a call to genuinely exhaust its retry budget, not
successfully rotate to a survivor). A write call burns through the
20-attempt budget in ~3ms (ECONNREFUSED classifies as zero-backoff
XCE_FO_RETRY_NOW, not the ~70-100s backoff track reserved for a
reachable-but-electing controller), returning the real -E_XCE_NOT_READY
(-10013) and triggering MarkDegraded(10s). Immediately after, a second
write call fast-fails in ~1ms via the literal ErrBackendUnavailable
string -- proving the real C call was skipped entirely, not just fast
-- while a concurrent GET /ping (confirmed by source read to never
touch the backend) keeps succeeding normally throughout.

Cross-corroborated across two independently-generated log subsystems
(apiserver's Go zap log and libxce's own C-level retry log) matching
to the millisecond. Bonus: confirmed the 10s TTL's half-open auto-clear
correctly re-attempts the real call afterward.

No src/ changes -- live validation only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNB4zGPCsHyDUCDJyMbiQN
2026-08-13 03:03:01 +00:00
04d41d29c6
docs(component-failover-adoption): mark task 5.2 complete, doubly confirms bug-039
Heartbeat-only idle-period E2E (apiserver alone, zero write traffic
during a 268s+ post-kill observation window) designed as a targeted,
cleaner confirmation experiment for task 5.1's bug-039 finding.

Result: bug-039 reproduces a second time, more cleanly than task 5.1
-- zero rejoin log lines throughout the observation window, and even
a post-window diagnostic write call (which itself succeeds against
the confirmed-new primary) triggers zero rejoin activity, ruling out
"task 5.1 was just unlucky timing." Controller-side logs corroborate
independently: zero "apiserver" references anywhere in the new
primary's log, meaning no join attempt of any kind ever reached it.

This task's own literal pass bar was not met -- approved on the same
basis as task 5.1: the validation itself succeeded rigorously and
honestly surfaced a real, now doubly-confirmed gap in the already-
shipped task 4.3/4.4 rejoin mechanism, which is the task's actual
purpose. A dedicated follow-up to fix heartbeat.go's transition
detection is warranted, separate from this spec's remaining scope.

No src/ changes -- live validation only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNB4zGPCsHyDUCDJyMbiQN
2026-08-13 02:38:30 +00:00
lei.chen
e5b3c42ab3 docs: sync design doc event:view rename + OpenWolf session bookkeeping
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-13 02:19:04 +00:00
lei.chen
673f2eb792 feat(apiserver): surface human-readable job ID (fmt_jobid form) in responses
The MCP/apiserver returned the raw 64-bit job ID (e.g. 73930964699447296)
while xce_test/xcecli display the dash form (e.g. 1-0-1786585485-0) that
parse_jobid accepts. A user taking the numeric form into xce_test fails to
parse it. The apiserver is the data-correctness owner (MCP only reads), so
it now emits the controller's fmt_jobid form alongside the numeric ID:

- models: JobSubmitResponse gains job_id_human; job list rows gain
  job_id_human (processJobData), using the existing backend.FormatJobID.
- schedulerclient: URMJob.HumanID + SubmitJobResult.JobIDHuman parse it.
- Tests: backend.FormatJobID (real t1 job, pre-cluster1 job, array child);
  processJobData adds job_id_human.

Also rename the SSE permission event:subscribe → event:view so mcp_readonly
stays strictly *:view (the read-only invariant test enforces).

Verified live on t1: scheduler_submit returns job_id_human 1-0-1786586474-0
and scheduler_get_job returns human_id.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-13 02:01:52 +00:00
6cb8f22b08
docs(component-failover-adoption): mark task 5.1 complete, full-cluster E2E validated
Live 4-process E2E run (3-node controller cluster + xcecd_virtual
worker + xce_scheduler + xce_apiserver, all on the full 3-candidate
list) with the primary killed mid-session. Worker reconnect is
solidly proven with real log evidence and unchanged PIDs across all
three components throughout. Scheduler's rejoin path was never
naturally exercised (no job reached its picker/allocator) -- same
partial-coverage shape already accepted for task 3.3.

Surfaced a likely-real, structural gap in the already-approved task
4.3/4.4 apiserver rejoin mechanism (bug-039): XCECLI_FF_CALL can
silently self-heal the transport within a single heartbeat tick,
making the cross-tick failure->success transition invisible to
heartbeat.go's detector on any failover fast enough to complete
within one heartbeat interval -- which is most real single-election
failovers in this codebase. Also found bug-040 (job pushes never
reach apiserver's store), very likely a downstream symptom of
bug-039's rejoin gap via on_apiserver_join()'s per-process,
unreplicated apiserver registration list. Both logged with detailed,
independently-verified root-cause analysis for a dedicated follow-up
-- fixing them is outside this task's cross-component-validation
boundary.

No src/ changes -- live validation only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNB4zGPCsHyDUCDJyMbiQN
2026-08-12 13:23:09 +00:00
79273a7f82
test(component-failover-adoption): close apiserver concurrency coverage gap
Audit of apiserver's three task-4.6 coverage areas (degraded-to-healthy
transitions, concurrent-request safety, candidate-list parity):
degraded-to-healthy and candidate-list parity were already fully
satisfied by tasks 4.1/4.5's own tests. Found one genuine gap: reconnect.go's
needsRejoin/checkAndPerformRejoin (task 4.3) had zero concurrent-access
test coverage despite being reached from 5 real concurrent HTTP-wrapper
call sites plus the heartbeat goroutine -- health.go's existing
50-goroutine stress test only covered the separate degraded-until
timestamp. One new test closes it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNB4zGPCsHyDUCDJyMbiQN
2026-08-12 12:21:54 +00:00
c8325e7575
feat(component-failover-adoption): add apiserver's backend-health circuit breaker
Adds an atomic degraded-until state (health.go: IsDegraded/MarkDegraded/
MarkHealthy) that lets request handlers fail fast with a clear
unavailable error instead of blocking on xcecli's own ~100s retry
budget when the backend is known-degraded.

Consolidates what tasks 4.3/4.4 left as duplicated inline
"call xcecli_*; checkAndPerformRejoin()" pairs across five wrapper
functions into one shared callBackend() helper: checks IsDegraded()
before every call (fail fast, skip the C call and the rejoin check
entirely on the degraded path -- checking rejoin there would itself
block up to ~100s, defeating the fail-fast purpose), marks degraded on
-E_XCE_NOT_READY, marks healthy at checkAndPerformRejoin()'s existing
success point (the single call site both the write path and the
idle-period heartbeat path already funnel through).

The circuit breaker's own TTL-based state is deliberately never read
by the rejoin-retry mechanism -- checkAndPerformRejoin() has no
IsDegraded() dependency, so a fast-fail window never suppresses
retry-until-actually-joined; heartbeat.go's unconditional per-tick
call keeps that cadence alive regardless of degraded state, and a
10s TTL (well under the ~100s retry budget) lets write traffic resume
contributing once it clears.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNB4zGPCsHyDUCDJyMbiQN
2026-08-12 10:52:10 +00:00
ecf5eddc61
feat(component-failover-adoption): trigger apiserver's rejoin from the idle-period heartbeat path
xcecli_heartbeat() is XCECLI_FF_CALL-wrapped -- a deliberately
non-retrying fire-and-forget path that does its own best-effort
one-shot reconnect but never calls xcecli_invoke_rejoin() (that only
fires from the write-call retry path). Left as-is, a controller
failover with no in-flight write request would silently repair the
transport connection without ever re-joining, permanently starving
push notifications.

runHeartbeat() gains two callback hooks fired at the right points --
onFailureToSuccess on a genuine transition, onSuccessTick on every
successful tick thereafter -- wired directly to task 4.3's existing
setNeedsRejoin()/checkAndPerformRejoin() from StartHeartbeat(). No
second rejoin mechanism: checkAndPerformRejoin() is a cheap no-op when
nothing is pending, so calling it unconditionally on every successful
tick is safe and matches the requirement that retry isn't gated on the
transition edge alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNB4zGPCsHyDUCDJyMbiQN
2026-08-12 09:58:27 +00:00
2a205291c0
feat(component-failover-adoption): add apiserver's rejoin logic with correctly-placed cgo registration
Registers a rejoin flag-setter (go_backend_rejoin_handler) as the
client library's reconnect callback: zero I/O, sets an atomic flag,
returns immediately -- avoiding self-deadlock on the library's
per-ctx failover lock the same way worker's and scheduler's rejoin
callbacks already do. The C shim, its extern declaration, and the
xcecli_set_reconnect_handler() registration call all live together in
clib.go's xce_backend_init(), placed after xcecli_start()'s success
check (xcecli_start() wipes any earlier-registered handler as part of
its own init -- the same ordering bug found and fixed in worker's/
scheduler's task 2.3/3.1 work).

A shared checkAndPerformRejoin() helper wraps every gCtx.ctx-based
backend call (SubmitJob, ControlJob, ControlQueue, ControlHost,
FetchFile): after the call returns, if a rejoin is pending, issue
xcecli_join() as a separate step and clear the flag only on success --
on failure it stays set so the next backend call's return retries it,
no separate retry loop.

needsRejoin is a proper sync/atomic flag rather than a plain bool,
since apiserver (unlike the single-threaded C daemons) serves
concurrent HTTP requests that can all reach the shared helper at once.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNB4zGPCsHyDUCDJyMbiQN
2026-08-12 09:16:19 +00:00
lei.chen
3d1560d69c chore(mcp): use XCE_API_KEY env var in xce-scheduler MCP config
Reference the operator key via ${XCE_API_KEY} instead of a hardcoded value —
apiserver restarts clear in-memory keys, so a committed key goes stale
silently and is a credential risk. Export XCE_API_KEY before starting Claude
Code to use the scheduler-agent MCP server.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 08:41:19 +00:00
lei.chen
11357a58f3 docs: mark Phase 2 (SSE events + WatchJobs + advanced tools) implemented
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 08:31:30 +00:00
lei.chen
59531d19ed feat(mcp-server): advanced tools — wait/analyze/suggest/estimate (Phase 2)
Add four tools closing the submit→watch→analyze→suggest loop in the
request/response MCP model:

- scheduler_wait_job: poll a job to a terminal state or timeout.
- scheduler_analyze_failure: gather job record + output logs for failure
  analysis (interpretation left to the LLM).
- scheduler_suggest_resources: conservative processor/node suggestions from
  current cluster availability.
- scheduler_estimate_wait_time: rough wait estimate from pending count and
  free cores (guidance, not a guarantee).

All verified live on t1 against the running apiserver.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 08:30:31 +00:00
lei.chen
c8bd8fe896 feat(schedulerclient): WatchJobs SSE stream + fix ListJobs filter param
Add WatchJobs(ctx) to the Client interface and HTTPClient: opens the
apiserver's /xce/v1/events SSE stream and yields JobEvent values until the
context is cancelled or the stream ends. JobEvent carries the event type,
job id, status string, exit code, and a raw job snapshot.

Also fix ListJobs' filter params: the apiserver accepts filters via the
"filter" query param in field:op:value form (e.g. queue_name:eq:normal);
the individual queue_name/submit_user params it ignores with a 400, which
broke every filtered list. Multiple conditions are brace-wrapped {a},{b}.

Verified live on t1: WatchJobs captured job.updated PEND→RUN→DONE for a
submitted job.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 08:30:25 +00:00
lei.chen
898de0cebd feat(apiserver): SSE job-event stream + EventBroker (Phase 2, ADR-004)
Add a Server-Sent Events endpoint /xce/v1/events streaming job lifecycle
events (job.created/updated/deleted/finished) to subscribers. The
DefaultHandler now publishes every job event to a process-wide EventBroker;
the SSE handler subscribes and streams JSON frames with a keep-alive comment
during idle.

- backend/broker.go: thread-safe fan-out Broker (drops for slow consumers so
  the notification path never blocks), Event type with job_status string.
- backend/handler.go: OnJob* publish to DefaultBroker.
- controllers/events.go: SSE handler (gin, text/event-stream, keep-alive).
- router + whitelist + rbac: /xce/v1/events route, event:subscribe
  permission granted to mcp_readonly/mcp_operator/cluster_admin.
- Tests: broker fan-out/slow-consumer/unsubscribe, handler publishes, SSE
  end-to-end via httptest.

Verified live on t1: SSE stream delivered job.updated PEND→RUN events for a
submitted job.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 08:30:18 +00:00