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
36 changes: 33 additions & 3 deletions .github/workflows/js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -242,18 +242,39 @@ jobs:
${{ matrix.run }} langchain_agent.mjs

callback-platforms:
name: Runtime callbacks (${{ matrix.os }})
name: Runtime callbacks (${{ matrix.name }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest]
include:
- name: macos-latest
os: macos-latest
- name: windows-latest
os: windows-latest
# `macos-latest` is arm64, so the shipped x86_64-apple-darwin binding
# only ever runs translated under Rosetta 2, where embedded-runtime
# start-up costs several times native. publish-js.yml was the only
# place that lane ran, so a timing regression on it first surfaced at
# release: v0.20.1 went red here while every native lane was green,
# and `release-js` is gated on these jobs, so npm was never reached.
- name: macos-latest x86_64 (Rosetta)
os: macos-latest
target: x86_64-apple-darwin
architecture: x64
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
# Keep the host and cross lanes in separate caches: they share a job
# and an OS, and a restored arm64 `target/` must not reach the x64
# build.
prefix-key: v0-rust-${{ matrix.target || 'host' }}
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 10.33.0
Expand All @@ -262,10 +283,19 @@ jobs:
node-version: "24.22.1"
cache: pnpm
cache-dependency-path: crates/bashkit-js/pnpm-lock.yaml
architecture: ${{ matrix.architecture }}
- run: pnpm install --frozen-lockfile
working-directory: crates/bashkit-js
- run: pnpm run build
- if: ${{ !matrix.target }}
run: pnpm run build
working-directory: crates/bashkit-js
- if: ${{ matrix.target }}
run: pnpm exec napi build --platform --release --target ${{ matrix.target }} && pnpm run build:cjs && pnpm run build:ts
working-directory: crates/bashkit-js
env:
# Match publish-js.yml, which is the other workflow building this
# target.
MACOSX_DEPLOYMENT_TARGET: "10.13"
- run: pnpm exec ava __test__/runtime-functions.spec.ts
working-directory: crates/bashkit-js
- run: node --test __test__/runtime-compat/runtime-functions.test.mjs
Expand Down
31 changes: 29 additions & 2 deletions crates/bashkit-js/__test__/runtime-functions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,27 @@ const request = new AsyncLocalStorage<string>();
const key = new Uint8Array(32).fill(7);
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

// Budget for the cases that let a host callback hang until the execution
// timeout cancels it. The clock starts at `execute` and so covers everything
// that happens before the host callback is dispatched -- interpreter start-up
// on first use in the process, plus scheduling latency while the rest of the
// suite runs concurrently. Only what is left over is the hang these cases
// actually mean to cancel. Too small a budget expires first, the run is
// cancelled before the callback is ever invoked, and no abort listener is ever
// registered: correct behaviour, but not what is under test.
//
// 2s was too small on x86_64-apple-darwin -- the shipped Intel binding, which
// runs translated under Rosetta 2 because `macos-latest` is arm64. In the
// v0.20.1 `Publish JS` run on that lane the first `python host functions` call
// alone took 3.7s, already past the whole budget, and three of these four
// variants failed there. The one that passed ran later, against an already
// warm runtime, and still took 4.7s of wall time for its 2s timeout. Keep this
// well clear of both numbers.
//
// Raising it slows these cases down by design, since they wait out the full
// timeout, but ava runs them concurrently: the cost is one timeout, not four.
const HOST_TIMEOUT_MS = 20_000;

for (const language of ["python", "typescript"] as const) {
const command =
language === "python"
Expand Down Expand Up @@ -171,8 +192,10 @@ for (const language of ["python", "typescript"] as const) {

test(`${Factory.name}: ${language} timeout aborts host work and permits reuse`, async (t) => {
let aborted = false;
let entered = false;
const bash = new Factory({
...options(async (_name, _args, _kwargs, ctx) => {
entered = true;
await new Promise((_resolve, reject) =>
ctx.signal.addEventListener(
"abort",
Expand All @@ -184,11 +207,15 @@ for (const language of ["python", "typescript"] as const) {
),
);
}),
timeoutMs: 2_000,
timeoutMs: HOST_TIMEOUT_MS,
});
t.not((await bash.execute(call)).exitCode, 0);
await delay(0);
t.true(aborted);
// Two different failures reach this point, and only one is about
// cancellation. Assert them apart so a run that timed out before the
// host callback started can never read as an abort regression.
t.true(entered, "host callback never ran; the timeout fired first");
t.true(aborted, "host callback ran but was never aborted");
t.is((await bash.execute("echo reusable")).stdout, "reusable\n");
});

Expand Down
14 changes: 13 additions & 1 deletion knowledge/runtimes/node-package.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,19 @@ async host-function shutdown contract.
`runtime-functions.spec.ts` exercises real HTTP request contexts, aborts, timeout,
queued calls, worker threads, adapters, reentry, bad values and snapshot/reset.
`runtime-compat/runtime-functions.test.mjs` runs in Node, Bun and Deno. Dedicated
macOS and Windows CI runs the callback suite as well as Linux's runtime matrix.
macOS and Windows CI runs the callback suite as well as Linux's runtime matrix,
including the shipped x86_64-apple-darwin binding translated under Rosetta 2 on
the arm64 macOS runner. That is the slowest lane shipped; until v0.20.1 only
`publish-js.yml` ran it, so a timing regression there first surfaced at release
and blocked `release-js`, and with it both npm packages.

Cases that cancel a hanging host callback must budget an execution timeout
covering interpreter start-up and scheduling latency before the callback is
dispatched, not just the hang itself. Too small a budget expires first, the run
is cancelled before the callback is invoked, and no abort listener is ever
registered, so the case reports a missing abort instead of a slow start. On the
Rosetta lane a first embedded-runtime call alone cost 3.7s, well past the 2s
budget that failed there; `HOST_TIMEOUT_MS` is kept far clear of it.

## Host runtime security

Expand Down
Loading