Skip to content

coop/in_process: second Next app dies with TypeError: value is not a function (GC rooting, multi-heap) #8546

Description

@proggeramlug

Summary

With #8528 merged, a Coop in_process daemon can load and initialize two
Next.js applications in one process — but the second application's HTTP
handler rejects at dispatch, so it serves 500 while the first serves 200.

Evidence

Measured on the quiet M1 mini, Perry 7c22189aa (0.5.1514), Coop in_process,
two deployments of the same Next fixture. Provider provenance verified by
content, not by recorded metadata: the merged fix deletes
ERR_PERRY_PATH_MODULE_THREAD, and strings on the packaged
libperry_runtime.dylib finds 0 occurrences of it (the string also appears
nowhere in main's source, so the check is meaningful rather than vacuous).

Both applications initialize cleanly:

INFO application library preloaded on dedicated Perry thread deployment="bench-000" load_ms=... init_ms=...
INFO application library preloaded on dedicated Perry thread deployment="bench-001" load_ms=749.8 init_ms=697.0

Then:

bench-000 -> 200  {"runtime":"next","iterations":100,"checksum":3726872593}
bench-001 -> 500  coop: 500 deployment dispatch failed

ERROR deployment dispatch failed deployment=bench-001
      error=Runtime(invoking deployment HTTP handler: handler promise rejected: )

The rejection reason is empty, which is itself worth fixing — an empty
handler promise rejected: gives an operator nothing to act on.

What this is NOT

  • Not the path-module registry. Zero rejected duplicate path-module initializer messages in this run; both apps register their own initializers
    on their own dedicated Perry threads. (An earlier iteration of fix(runtime): make the path-module registry per-heap so one process can host several apps #8528 that kept
    the initializer-address map process-wide DID produce 115 such rejections;
    that is fixed.)
  • Not app load or module init — both complete, with timings logged.
  • Not ordering-dependent in an obvious way: the first deployment to be
    dispatched succeeds and the second fails.

Hypothesis

Some other process-global runtime/stdlib state assumes a single application per
process. Until #8528 the fail-closed thread guard made multi-app hosting
impossible, so none of that state has ever been exercised by two heaps. The
per-request registries are the obvious suspects — several are Mutex<HashMap<usize, ...>>
and are UNCOVERED by scripts/gc_runtime_root_holders.py, i.e. nobody has
determined whether they hold heap pointers:

REQUEST_REGISTRY, BLOB_REGISTRY, FETCH_RESPONSES, TRANSFORM_PAIRS,
STREAM_HANDLES (crates/perry-stdlib/src/fetch/mod.rs,
streams/transform.rs), plus buffer/own_props.rs's PROPS and
buffer/header.rs's EXTERNAL_*_REGISTRY (the latter keyed by RAW heap
address).

Suggested first steps

  1. Make the rejection reason non-empty — surface the underlying JS error.
  2. With that, name the failing operation, then check whether its backing table
    is process-global.

Reproduction

Needs a Coop checkout whose perry-main.lock points at a Perry with #8528, plus
coop-daemon's rusqlite bumped 0.37 -> 0.39 to match perry-stdlib (they link
the same native sqlite3; see the companion Coop change). Then
scripts/prepare-next-benchmark.sh and the resource_benchmark harness with
COOP_BENCH_APP_COUNTS=2, COOP_BENCH_EXECUTION_MODE=in_process.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions