You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 0f2fdcd
Browse filesBrowse the repository at this point in the historyBrowse files
fix(core)!: a throwing kernel:bootstrapped / kernel:listening handler fails the boot on LiteKernel too (#5257) (#5275)
#5170 (PR #5258) unified `kernel:ready` across the two kernels and deliberately
ruled that hook ONLY, pinning the rest as "still fail-soft on LiteKernel" so
widening it would have to be a deliberate change. This is that change, for the
two hooks that are still on the BOOT path.
`kernel:bootstrapped` and `kernel:listening` now use the propagating dispatcher
(`triggerHookOrThrow`) on LiteKernel, in the shape #5258 established: remaining
handlers skipped, later boot hooks never fired, original error unwrapped,
`state = 'stopped'`, and no "✅ Bootstrap complete".
The failure this removes is the ugliest one available. HonoServerPlugin opens
its socket inside a `kernel:listening` handler — `await server.listen(port)`,
with no try/catch of its own, deliberately. When that rejected on LiteKernel
(EACCES on a privileged port, a failure inside the port-fallback logic, an
edge/serverless host where listen is unavailable) the throw was swallowed,
`bootstrap()` resolved, and the process printed "✅ Bootstrap complete" with
nothing listening — the health check after it was the first thing to notice, and
it had already been told startup succeeded. The same plugin code on ObjectKernel
failed the boot. Plain port-in-use never triggered it (server.listen falls back
to a random port internally), which is why it stayed invisible.
`kernel:shutdown` KEEPS fail-soft dispatch, now as an explicit per-hook
judgement recorded at the dispatch site rather than an inherited default: on the
teardown path the handlers queued behind a failing one — and the reverse-order
destroy() pass after them — are what flush buffers and release resources.
#5258's pin "keeps fail-soft dispatch for hooks other than kernel:ready" covered
all three hooks in one assertion; it is split here into the three tests that are
now true, per hook, so the flip is visible in the diff. Symmetric regressions
added on BOTH kernels for bootstrapped and listening (incl. asserting the
success line is never logged).
Evidence: every non-test `kernel:bootstrapped` subscriber already guards itself
(objectql's announceOpenMigrationGates catches internally — "an advisory must
never be the reason a boot fails"; plugin-sharing wraps each of its five passes;
plugin-pinyin-search wraps its backfill; service-automation's audit is
in-memory reads behind an `if (!this.engine) return`). Suites: core 426,
runtime 1313, client 222, http-conformance 46, service-automation 665,
connector-{rest,mcp,slack,openapi} 79, plugin-hono-server 164, plugin-sharing
347, plugin-pinyin-search 14 — all pass.
Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
Co-authored-by: Claude <noreply@anthropic.com>
`ctx.hook(name, handler)` takes exactly two arguments — there is no options/priority parameter. Kernel hooks run in **registration order**, and `ctx.trigger()` awaits each handler sequentially.
35
35
36
-
A `kernel:ready` handler that **throws fails the boot**, on `ObjectKernel` and `LiteKernel` alike: the remaining `kernel:ready` handlers are skipped, `kernel:bootstrapped` and `kernel:listening` never fire, and `bootstrap()` rejects with the original error. That is what makes `kernel:ready` the place to assert that a precondition your plugin *declared* was actually met — the service registry is still filling during `init()`, so nothing earlier can judge it, and a deployment that cannot honour what it announced must refuse to start rather than serve without the guarantee. The other lifecycle hooks do **not** share that contract: `ObjectKernel` propagates their failures too, while `LiteKernel` logs each one and runs the remaining handlers. Put boot assertions in `kernel:ready`.
36
+
A handler on any of the three **boot-path** hooks — `kernel:ready`, `kernel:bootstrapped`, `kernel:listening` — that **throws fails the boot**, on `ObjectKernel` and `LiteKernel` alike: the remaining handlers for that hook are skipped, the later boot hooks never fire, `bootstrap()` rejects with the original error (unwrapped), the kernel is left `stopped` rather than `running`, and **no "✅ Bootstrap complete" is logged**. Everything dispatched before that line is a precondition of it, so swallowing a failure there would not rescue the boot — it would only hide it behind a process reporting success. That is most visible on `kernel:listening`, where HTTP server plugins open their socket: a `listen()` that rejects (EACCES on a privileged port, a host that cannot listen at all) takes the boot down instead of leaving a live process with nothing listening.
37
+
38
+
`kernel:ready` is still the right place for **boot assertions** specifically — the service registry is only finished filling by then, so nothing earlier can judge whether a precondition your plugin *declared* was actually met, and a deployment that cannot honour what it announced must refuse to start rather than serve without the guarantee.
39
+
40
+
`kernel:shutdown` is the deliberate exception: on `LiteKernel` a failing shutdown handler is logged and the remaining cleanup still runs, because the handlers queued behind it — and the `destroy()` pass after them — are what flush buffers and release resources, so aborting teardown only leaks what they were about to release. `ObjectKernel`'s shutdown path does not yet match that (tracked in [#5274](https://github.com/objectstack-ai/objectstack/issues/5274)); write shutdown handlers that handle their own errors either way.
0 commit comments