feat(scheduler-core): implement REPL breakpoints and state inspection (task 12.2)

Adds the interactive simulation REPL's breakpoint and state-inspection
commands (Req 5 AC 22): `break <event_type> [key=value ...]` / `info
break` / `delete <id>|all` on event and decision types, and `info
session/job/queue/worker/resource/summary`.

repl.c (370 lines after task 12.1) couldn't hold this task's full
surface under the project's 800-line cap, so it's split three ways
(matching the snapshot.c/snapshot_wire.c/snapshot_load.c precedent):
repl.c (execution control + dispatch), repl_break.c (matching/CRUD +
the three new driver-facing hooks), repl_info.c (info dumps), sharing
state via repl_internal.h. sim_loop.c gained 3 new bare hook calls
(repl.h inline wrappers, same pattern as task 12.1's two) at the
points design.md's pseudocode requires -- per decoded event before it
joins the batch, per session boundary, and per drained decision --
landing sim_loop.c at exactly 800/800 lines.

Two real bugs found and fixed during development (bug-1506, bug-1507):
an uninitialized xstr_table left breakpoint filter fields silently
NULL instead of failing loud, and repl_breakpoint_free() called a bare
free() on an RTTI-refcounted makeString() string, corrupting the heap.

Review found one gap: `info job` printed only a resreq presence flag
instead of the actual resreq text, when format_resreq() already
existed in this repo for exactly this purpose. Fixed to match.

Scheduler suite: 35/35 clean (was 34/34). RED-phase evidence (2 real
bugs plus 2 synthetic probes, all reverted with confirmed zero
residue) recorded in tasks.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JPPXeKSUXUqcqvzNXY1rzJ
This commit is contained in:
Rongsong Shen 2026-08-07 09:46:16 +00:00
parent 02f7f48ff9
commit 2fecea3592
Signed by: rshen
SSH key fingerprint: SHA256:8Vf2pZ3h6rJa7EhGK+WaAQ9Mta/apZvteW6g9U/WKKk
18 changed files with 3067 additions and 212 deletions

View file

@ -14840,5 +14840,48 @@
],
"occurrences": 1,
"last_seen": "2026-08-07T08:10:00Z"
},
{
"id": "bug-1506",
"timestamp": "2026-08-07T09:05:00Z",
"error_message": "sched_repl_breakpoints_inspection_test: repl_dispatch(sim, \"break JOB_STATE_CHANGE jobid=12345\") stored NULL for the breakpoint's token/filter key/value -- TEST_ASSERT_EQUAL_STRING(\"jobid\", ...) failed with 'Expected jobid Was NULL'.",
"file": "src/scheduler/testsuite/test_repl_breakpoints_inspection.c",
"root_cause": "makeString() (src/lib/xstring.c) is an interned-xstring allocator backed by a process-global xstr_table that must be initialized via xstring_table_init() before first use -- xce_sched_core_init() (core.c) is the only call site in the scheduler tree and does this transitively. New task 12.2 unit tests built a bare xce_sched_sim_driver_t via memset(0) and called repl_state_ensure()/repl_dispatch() directly for pure command-dispatch tests, without ever calling xce_sched_core_init() first (the real driver flow always does, via sched_sim_driver_start() before xce_repl_start() runs, so this precondition is always satisfied in production -- only the test fixtures skipped it). With xstr_table uninitialized (NULL), makeString()'s internal key_find()/key_add() against a NULL kvmgr container fail gracefully and return NULL rather than crash, so the bug surfaced as silently-NULL breakpoint fields, not an immediate crash.",
"fix": "Added xce_sched_core_init(&(sim.core)) as the first statement (right after memset) in every Group A/B test in test_repl_breakpoints_inspection.c that calls repl_dispatch()'s break/delete commands or the repl_break.c matcher functions -- matching the real driver's own precondition ordering.",
"tags": [
"scheduler-core",
"task-12.2",
"repl",
"xstring",
"test-fixture",
"makeString"
],
"related_bugs": [
"bug-1507"
],
"occurrences": 1,
"last_seen": "2026-08-07T09:05:00Z"
},
{
"id": "bug-1507",
"timestamp": "2026-08-07T09:10:00Z",
"error_message": "sched_repl_breakpoints_inspection_test: 'free(): invalid pointer' SIGABRT inside repl_breakpoint_free(), reached from repl_cmd_break()'s malformed-filter rejection path (\"break JOB_PUSH nofiltermark\"). gdb backtrace: malloc_printerr <- repl_breakpoint_free (repl_break.c:375) <- repl_cmd_break <- repl_dispatch <- test_break_malformed_filter_rejected.",
"file": "src/scheduler/src/repl_break.c",
"root_cause": "repl_breakpoint_free() called FREEUP() (a bare free()) on bp->token and each filter's key/value, all of which are makeString()-produced xstrings -- RTTI reference-counted objects (src/lib/xstring.c: xstring_type has custom .reference/.dereference interface functions; the returned pointer is the object's DATA region inside an RTTI-managed allocation, not a plain malloc() chunk). Releasing one requires going through the RTTI dereference path (xstring_dereference(), which only fires the real free when the refcount hits zero via the owning kvtab's key_del), never a bare free(). This codebase's own kill_sched_job()/kill_sched_queue() (schedobj.c) establish the identical precedent for every other makeString()'d field in the scheduler tree (xce_sched_job_t.submit fields, xce_sched_queue_t.name, etc.): none of them are ever explicitly released either -- they live for the process's lifetime, matching this whole test suite's 'detect_leaks=0 by design' convention. My new repl_break.c code was the first place in this file family to call free() directly on a makeString() result.",
"fix": "Removed the FREEUP() calls on bp->token/filters[i].key/filters[i].value from repl_breakpoint_free() (repl_break.c) -- it now only frees the plain calloc'd bp->filters array and bp itself, matching kill_sched_job()/kill_sched_queue()'s established never-release-xstrings precedent. Updated repl_internal.h's xce_repl_filter_t doc comment to state this explicitly so a future maintainer doesn't reintroduce the same free() on an RTTI-managed pointer.",
"tags": [
"scheduler-core",
"task-12.2",
"repl",
"xstring",
"heap-corruption",
"free",
"rtti"
],
"related_bugs": [
"bug-1506"
],
"occurrences": 1,
"last_seen": "2026-08-07T09:10:00Z"
}
]