Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .changeset/kernel-resolver-resolve-environment.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
---
"@objectstack/runtime": minor
"@objectstack/rest": patch
---

`KernelResolver` gains an optional environment-only member so a REST request
pays ONE kernel-waiter window instead of two (#10988).

`RestApiPlugin` wraps the host's ADR-0006 `kernel-resolver` so `RestServer` can
ask "which environment is this request in?". It asked `resolveKernel` — a
kernel-ACQUISITION api — and kept only `context.environmentId`. A host resolver
writes the id and then awaits that environment's kernel, so the wrapper paid a
full waiter window and discarded what it bought; `resolveProtocol` then acquired
the kernel again. Free on a warm environment (a cache hit, which is why this was
invisible), a second serial wait on a cold or wedged one. Measured on a live
multi-tenant host with `waiterTimeoutMs: 20s`: REST-owned routes
(`/api/v1/discovery`, `/api/v1/data/:object`) answered 503 after ~42s where
dispatcher-owned routes answered after ~21s.

`KernelResolver.resolveEnvironment?(context, defaultKernel)` resolves ONLY the
request's environment onto the context, acquiring no kernel; the REST wrapper
prefers it when the host implements it, leaving `resolveProtocol` as the single
kernel-acquisition point on the path.

**Non-breaking, and no flag day.** The member is `?.`-optional: a host that
implements only `resolveKernel` type-checks and behaves exactly as before (it
keeps paying the discarded acquisition on cold builds), so this ships before any
host implements the new half. Adding an optional member to an interface the
framework CONSUMES cannot invalidate an existing implementation — every resolver
already in the field still satisfies the contract. Marked `minor` on
`@objectstack/runtime` because it is a new public capability on an exported
contract, `patch` on `@objectstack/rest` because the wrapper change is a fix
with no surface of its own.

Fail-closed is unchanged and pinned: the surviving `getOrCreate` still rejects
for a genuinely unavailable kernel, so the caller still gets the host's declared
503 — a shorter wait to the same verdict, never a response served against no
kernel. `waiterTimeoutMs` is a host setting and is untouched; the defect was
waiting twice, not waiting wrong.
32 changes: 31 additions & 1 deletion packages/rest/src/rest-api-plugin.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -192,7 +192,37 @@ export function createRestApiPlugin(config: RestApiPluginConfig = {}): Plugin {
// resolver strategy starts keying off routePath,
// add prefix-stripped assembly here.
const context: { request: unknown; environmentId?: string } = { request: req };
await kernelResolver.resolveKernel(context, hostKernelFacade);
// Ask the environment-only question when the host
// can answer it. This wrapper wants an ID and
// nothing else; `resolveKernel` is a kernel-
// ACQUISITION api, and the kernel it hands back
// here is discarded on the success path and lost
// with the rejection on the cold one. That discard
// is free on a warm environment and is a whole
// waiter window on a cold or wedged one — measured
// on a live host with `waiterTimeoutMs: 20s`,
// REST-owned routes (`/api/v1/discovery`,
// `/api/v1/data/:object`) answered 503 after 42s
// against a wedged environment because
// `resolveProtocol` then opens a SECOND window to
// acquire the kernel for real. Preferring
// `resolveEnvironment` leaves `resolveProtocol` as
// the single acquisition point, so one request
// pays one window.
//
// ⛔ No fallback to `resolveKernel` when this
// returns without setting `environmentId`: an unset
// id is the seam's FINAL answer for an unscoped /
// control-plane request (see
// `RestRequestEnvResolver`), and "retry with the
// expensive method" would re-buy exactly the window
// this prefers away, on precisely the requests that
// need no environment at all.
if (typeof kernelResolver.resolveEnvironment === 'function') {
await kernelResolver.resolveEnvironment(context, hostKernelFacade);
} else {
await kernelResolver.resolveKernel(context, hostKernelFacade);
}
return context.environmentId;
},
};
Expand Down
Loading
Loading