Skip to content

The emulator port-retry test exercises the retry and waits for the daemon; PRISMA_COMPOSER_EMULATORS_DIR picks the registry - #333

Merged
wmadden-electric merged 7 commits into
mainfrom
fix/dev-emulators-port-retry
Oct 5, 2026
Merged

wmadden-electric merged 7 commits into
mainfrom
fix/dev-emulators-port-retry

Conversation

@wmadden-electric

@wmadden-electric wmadden-electric commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Found while landing the one-config-file project (prisma/orm#30536): the dev-emulators suite failed the port-retry test on four of six CI runs of two branches, while main stayed green.

(fail) fresh-allocation port retry (spec § 2 step 5) > a bind failure on a fresh allocation retries the next free port
error: expect(received).toHaveLength(expected)
Expected length: 1
Received length: 0

What was wrong

Two things, both in the test.

It raced the daemon's log line. ensureDaemon returns on the first good health check. Under Bun the compute daemon answers /health before its listen callback has written the "listening on" line, and the test read the log straight away. Measured by starting the built daemon the way ensureDaemon does and polling health: the line was missing at the first good check in roughly one start in six, and present 300 ms later every time. Writing the line synchronously does not help; the callback itself runs late. Which branch hits it is timing: main's CI run of this test took 2.1 s and the failing branch run 1.26 s.

It never exercised the retry. The test "squatted" a port it got from get-port, but get-port locks every port it hands out inside the process, so the squatted port was never allocated to the daemon and the first bind succeeded. The retry path the test is named for had no coverage.

The fix

The retry test starts the daemon through a fixture that, on its first start, binds its own allocated port and then runs the real daemon, so the bind fails for real and ensureDaemon retries. It asserts the daemon ends on a port above the taken one, and waits for the listening line instead of reading the log immediately. The concurrent-ensure test waits for the line the same way. With retries disabled the new test fails with "compute emulator failed to start on port 4307".

Emulator copies per checkout or CI job

The daemons are machine-wide by design, registered under ~/.prisma-composer/emulators, and the daemon API already takes a registryRoot so copies can coexist. Nothing let a user or CI choose one. PRISMA_COMPOSER_EMULATORS_DIR now sets the registry root; a relative value is refused with DEV.EMULATORS_DIR_INVALID. The three local-dev integration proofs read their root from the new emulatorRegistryRoot() export on @prisma/composer-prisma-cloud/local-target instead of hardcoding the home path. The running-locally guide and the skill describe the variable. Not covered: @prisma/dev keeps its own Postgres server records outside this directory, so two checkouts still share those.

Review outcome

The architect and principal-engineer passes (in prisma/orm#30536, slices/04-emulator-port-retry/review/) found one thing the first version understated: the variable isolates the compute and buckets daemons, their registry and their data, but not local Postgres. Two checkouts of one app get the same local Postgres server names, so the second checkout's emulator adopts the first's running server, and --fresh or teardown in either stops that server and deletes its database for both. The guide, the skill and the local-dev design doc now say exactly that. Smaller items from the review: the settle wait requires two quiet windows and has a deadline; the value is normalised and the error says ~ is not expanded; the variable name is an exported constant; the error reaching the CLI is tested through the engine harness.

Follow-ups, not in this PR

  • Include the registry root in local Postgres instance names when the variable is set, so each root gets its own Postgres server.
  • The "held after 5 consecutive fast crashes" test allows 20 s while its backoff alone takes 15 s, a likely flake under load; raise the budget or make the backoff base injectable.

Verification

  • The four new registry-root tests failed before the implementation and pass after; the retry test fails with retries disabled.
  • Root lint, typecheck, lint:deps and check:skill-packaging pass. test/integration's bun test passes.
  • A local-dev proof run with the variable set to a scratch directory started its daemons on 4303, 4305 and 4307 beside the home daemons on 4304 and 4306, wrote its lock files under the scratch directory, and left the home registry byte-identical.

Agent: columbo-17

🤖 Generated with Claude Code

@prisma-gizmo

prisma-gizmo Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

✅ Gizmo reviewed 60973f1 — posted 0 inline comment(s) this pass.

Open findings: 🟡 2 minor

Change walkthrough

This PR fixes a flaky dev-emulators port-retry test that never actually exercised the retry (the old test squatted a port that get-port had already locked in-process, so the first bind always succeeded; it also read the daemon's log before the listen callback had written the "listening on" line), and adds PRISMA_COMPOSER_EMULATORS_DIR so a checkout or CI job can get its own emulator daemons instead of sharing the machine-wide ~/.prisma-composer/emulators registry.

Test reliability (daemon.test.ts, helpers.ts, port-taken-on-first-start.ts). The retry test now starts the daemon through a fixture that binds its allocated port itself on first start, so ensureDaemon's fresh-allocation retry path runs for real; the shared awaitListeningLines helper waits for the log line and requires two quiet 500 ms windows before asserting counts, which the concurrent-ensure test uses too. The fixture's args match what daemon.ts spawnDaemonProcess passes, and the asserted listening-line string matches compute-main's output exactly.

Registry root (daemon.ts, local-target.ts). defaultRegistryRoot() now reads the env var, normalises the value, and refuses relative or ~-prefixed paths with a structured DEV.EMULATORS_DIR_INVALID error (registered in ADR-0044). The variable is exported as EMULATORS_DIR_ENV, re-exported through the published local-target entry as emulatorRegistryRoot() (with the ambient .d.ts kept in sync), which is how the three local-dev integration proofs replaced their hardcoded home path. The existing { registryRoot } test seam stays test-only — production emulators.ts relies on the default, so the env var is the single production knob.

Coverage (dev-emulators-dir.test.ts). A new CLI-level test proves the structured error reaches the dev run envelope unchanged; it duplicates the error's strings because the CLI package can't import @internal/dev-emulators, as its header documents.

Docs (design doc, guide, skill). All three state the same caveat: the variable isolates compute/buckets daemons, registry, and data, but not local Postgres — two checkouts of one app still share a server, with --fresh/teardown in either affecting both.

@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 535010e2-a45b-4538-9418-afb0dba542c7
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@prisma/composer@333
npm i https://pkg.pr.new/@prisma/composer-cli@333
npm i https://pkg.pr.new/@prisma/composer-prisma-cloud@333

commit: 60973f1

prisma-gizmo[bot]
prisma-gizmo Bot previously requested changes Oct 1, 2026

@prisma-gizmo prisma-gizmo Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New findings: 🟡 2 minor · trace

Comment thread docs/guides/running-locally.md Outdated
Comment on lines +115 to +123
- **`PRISMA_COMPOSER_EMULATORS_DIR` gives a checkout or a CI job its own
emulators.** By default every `dev` on the machine shares one set of
emulator daemons, registered under `~/.prisma-composer/emulators`. Set the
variable to an absolute directory and `dev` starts and finds its own
emulators there instead, with their own data, so two checkouts or two CI
jobs never share a daemon by accident. Both pick free ports, so they can
run side by side. A relative path is refused with `DEV.EMULATORS_DIR_INVALID`.
`emulatorRegistryRoot()` from `@prisma/composer-prisma-cloud/local-target`
returns the directory in effect.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor · correctness — Guide overstates isolation: Postgres data is not scoped by PRISMA_COMPOSER_EMULATORS_DIR

The new bullet says setting PRISMA_COMPOSER_EMULATORS_DIR gives a checkout or CI job its own emulators "with their own data, so two checkouts or two CI jobs never share a daemon by accident". That is true for the compute/buckets/postgres daemons and their registries, but — as the PR description itself notes — @prisma/dev keeps its own Postgres server records outside this directory, so two checkouts with different PRISMA_COMPOSER_EMULATORS_DIR values still share the local Postgres data and the machine-wide Postgres session. A reader isolating a CI job for data-separation reasons (not just daemon-port reasons) will not get the isolation the bullet implies. The existing "local Postgres is one shared session" bullet below covers the session, not the server records.

Recommended fix

Add a caveat sentence to the new bullet, e.g. "The @prisma/dev Postgres server records stay machine-wide regardless of this variable, so two registry roots still share the local Postgres data."

Comment on lines +56 to +69
/**
* The daemon's listening lines, once at least one is in its log. Under bun,
* the daemon can answer /health before its `listen` callback writes the line.
*/
async function awaitListeningLines(logPath: string): Promise<string[]> {
const listeningLines = () =>
fs
.readFileSync(logPath, 'utf8')
.split('\n')
.filter((line) => line.includes('listening on 127.0.0.1:'));
await waitFor(() => listeningLines().length > 0, 5000);
return listeningLines();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor · tests — awaitListeningLines stops at the first line, so the concurrent-ensure count can still race a duplicate line

awaitListeningLines resolves as soon as the first listening on line appears, and the concurrent-ensure test immediately asserts toHaveLength(1). A losing racing spawn's listening line — the exact failure mode this test exists to catch — that lands after the first successful poll is never seen, so the assertion can pass while the protocol violation is still in flight. This is not a regression (the old code read the log immediately, a strictly worse window), but since this PR reworks this line to stop racing the daemon, waiting only for the first line still leaves the assertion able to outrun the evidence. The retry-test caller is unaffected: its failed start provably cannot log a listening line.

Recommended fix

After the first listening line appears, let the helper settle before returning — e.g. sleep ~500 ms and re-read the log — so a second spawn's line has time to show up before the count is asserted.

@wmadden-electric
wmadden-electric force-pushed the fix/dev-emulators-port-retry branch from fdfcddc to 27562e6 Compare October 5, 2026 09:53

@prisma-gizmo prisma-gizmo Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New findings: none · trace

Still open from previous reviews: 🟡 2 minor

@wmadden-electric
wmadden-electric dismissed prisma-gizmo[bot]’s stale review October 5, 2026 10:09

Both findings addressed in the follow-up commits; the bot's re-review reports no new findings.

@prisma-gizmo prisma-gizmo Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New findings: none · trace

Still open from previous reviews: 🟡 2 minor

wmadden-electric and others added 7 commits October 5, 2026 15:48
…s for the listening line

The test squatted a free port and expected ensureDaemon to fail on it and
retry. It never did: get-port probes each candidate and locks the ports it
returns in-process, so the allocation skipped the squatted port and the first
spawn succeeded. The retry path went untested.

It then read the daemon log as soon as ensureDaemon returned. Under bun the
daemon answers /health before its listen callback writes the listening line,
so the log can still be empty. That fails CI's Linux Test job with
"Expected length: 1, Received length: 0".

A fixture entry now binds its own allocated port on the first start, so the
daemon's bind fails for real and ensureDaemon retries the next free port. The
test and the concurrent-ensure test wait for the listening line before
counting it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…registry root

Every dev session on a machine shares one set of emulator daemons under
~/.prisma-composer/emulators. Two checkouts, or a CI job and a test suite,
could only use separate daemons through the test-only { registryRoot }
option.

defaultRegistryRoot() now returns $PRISMA_COMPOSER_EMULATORS_DIR when it is
set. A relative value throws DEV.EMULATORS_DIR_INVALID naming the variable,
because each process that resolves it would otherwise get a different
directory. ADR-0044's DEV subcode list gains that code.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…nd docs use it

@prisma/composer-prisma-cloud/local-target exports emulatorRegistryRoot(),
the same function the daemons resolve their registry with. The local-dev
integration proofs read their registry root from it instead of hardcoding
~/.prisma-composer/emulators, so with PRISMA_COMPOSER_EMULATORS_DIR set they
check the daemons they actually started.

The running-locally guide and the core-concepts skill say the variable gives
a checkout or a CI job its own emulators.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…data

@prisma/dev keeps its server records machine-wide, so two registry roots
still share local Postgres data.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…re asserting one spawn

The helper returned on the first listening line, so a losing spawn that
logged later could slip past the concurrent-ensure assertion.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…t it as EMULATORS_DIR_ENV

The variable's name is now the exported EMULATORS_DIR_ENV constant, used in
the code, the error and the tests, and re-exported from the local-target
entry next to emulatorRegistryRoot(). An absolute value is normalised with
path.resolve, so trailing separators and dot segments no longer give two
spellings of one directory. The error's fix says that `~` is not expanded.

A CLI test runs `dev` through the engine with an emulators hook that throws
DEV.EMULATORS_DIR_INVALID and checks the envelope carries its code, why,
meta and fix unchanged.

The listening-line wait now needs two consecutive quiet 500 ms windows, fails
if the count never settles, and treats a missing log as no lines. The
skipContendedDaemonPorts comment says the daemon starts above the port it
finds, because get-port holds that port. The local-target entry's comment and
its typecheck shadow list the new exports.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
… local Postgres

Local Postgres servers are named after the app and database, so two
checkouts of one app under different registry roots share a server: the
second adopts the first's, and --fresh or teardown in either stops it and
deletes the database for both. The guide and the skill now say so, and that
the variable isolates the compute and buckets daemons and their registry.

The local-dev design doc's machine-scoped emulators item describes the
registry root and the variable, and notes that two Composer versions on one
root keep replacing each other's daemons.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric
wmadden-electric force-pushed the fix/dev-emulators-port-retry branch from 4fb6584 to 60973f1 Compare October 5, 2026 13:59

@prisma-gizmo prisma-gizmo Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New findings: none · trace

Still open from previous reviews: 🟡 2 minor

@wmadden-electric
wmadden-electric merged commit ce98092 into main Oct 5, 2026
20 checks passed
@wmadden-electric
wmadden-electric deleted the fix/dev-emulators-port-retry branch October 5, 2026 14:11
RyanGarber pushed a commit to RyanGarber/prisma-orm-tmep that referenced this pull request Oct 9, 2026
```
$ git diff --stat main...HEAD | tail -1
 25 files changed, 1 insertion(+), 2833 deletions(-)
```

This closes the one-config-file project. It deletes the project's
working folder, `projects/one-config-file/`, and fixes one duplicate
number in the failure-mode catalogue. Linear: TML-3340.

The project moved Prisma Composer's configuration out of its own
`prisma-composer.config.ts` into the `composer` section of
`prisma.config.ts`, and deleted Composer's standalone `prisma-composer`
binary. Prisma 8 now has one CLI and one config file. The rest of this
description is the close-out record the Drive process asks for: what was
checked, where each decision now lives, and where each unfinished item
is tracked.

## What was delivered

| PR | What it did |
| --- | --- |
| prisma/composer#328 | Composer's configuration is the `composer`
section of `prisma.config.ts`. The old file and `configPath` are
refused. |
| prisma/composer#331 | The `prisma-composer` binary is gone. Docs,
examples and the shipped skill say `prisma deploy` and `prisma dev`.
Released as Composer 0.26.0. |
| prisma/prisma-cli#330 | The `prisma` host runs Composer 0.26.0.
Released as `prisma@8.0.0-rc.20`. |
| prisma/web#8387 | The public Composer docs describe the `composer`
section. |
| prisma/composer#332 | Found during testing: `prisma dev` and `prisma
deploy` failed in pnpm projects. Merged, not yet released (TML-3520). |
| prisma/composer#333 | An emulator test race that made CI flaky, plus
`PRISMA_COMPOSER_EMULATORS_DIR`. |

Three more PRs came out of this close-out and are open:

- prisma/composer#347 writes ADR-0050, which records the binary's
retirement. Without it the close-out failed the ADR audit, and four
older ADRs still described `prisma-composer` as the entry point.
- prisma/web#8415 fixes a tutorial page that still told readers to write
`prisma-composer.config.ts`.
- prisma/pdp-control-plane#5608 makes the platform's Compute import flow
write the `composer` section into `prisma.config.ts`, and recognise
repositories that already have it. Until now it wrote
`prisma-composer.config.mjs` and pinned Composer 0.25.0, so imported
repositories broke on upgrading to 0.26.0.

## Definition of Done

| Item | Verdict | Evidence |
| --- | --- | --- |
| orm-demo has one config file, and `prisma deploy` and `prisma dev` run
against it from the host | Met, with deviations | `examples/orm-demo`
has only `prisma.config.ts`. `prisma dev` from the host build reached
ready (slice 3 QA). A real `prisma deploy` of orm-demo succeeds in
Composer's e2e workflow on `main`, using the published host with the
workspace family. No deploy ran from the host build itself, because no
service token was available. |
| The old file and `configPath` get their diagnostics | Met, with a
deviation | `CONFIG.FILE_RETIRED` and `CONFIG.FIELD_RETIRED`, exit 2,
from the host binary. Shown with `prisma dev`, because `deploy` checks
credentials before reading the config. Both commands use the same
validator. |
| A broken `effect` install fails with `CLI.CONFIG_UNREADABLE`, and
`prisma --version` still works | Met, with the same deviation | Slice 3
QA, step 5. |
| Published packages have no `bin` and no stale name | Met |
`@prisma/composer-cli` and `@prisma/composer` 0.28.0 declare no `bin`.
Their unpacked tarballs name the old file only in the messages that
refuse it. |
| A CI check keeps the old name out | Met | `pnpm
lint:retired-binary-name` runs in CI. Its test plants a mention and
expects a failure. |
| TML-3340 Done, web pages updated | Met | TML-3340 is Done with a
closing comment. One page missed by #8387 is fixed in prisma/web#8415. |
| The consolidation plan says `deploy` and `dev` stay bare | Met |
`projects/consolidate-clis/cli-consolidation-plan.md`, and now ADR-0050
in prisma/composer. |
| Retro run, ADR merged, folder deleted | Met once #347 and this PR
merge | The retro's lesson is failure mode F42. ADR-0049 is merged.
ADR-0050 is in #347. |
| Repository references to the folder removed | Met | Nothing outside
the folder links to it. |
| Manual QA for each user-facing slice | Met, with a deviation | Slice 3
has a QA transcript. Slices 1 and 2 recorded their manual QA in the
Verification sections of #328 and #331. |

## Where each decision lives now

Every decision recorded in the deleted spec and design notes has a home
outside the folder:

- **Composer's configuration is the `composer` section, validated by the
section, with the old file and field refused:** ADR-0049, and the
`CONFIG` code list in ADR-0044.
- **The `effect` version pre-flight is deleted; a broken tree fails with
the engine's error:** ADR-0049. #347 adds the four rejected
alternatives, which until now were only in #328's description.
- **The binary is retired, and `destroy` and `log` stay programmatic:**
ADR-0050 in #347.
- **`deploy` and `dev` stay bare commands; `destroy` is not mounted:**
ADR-0050, and the consolidation plan.
- **The examples' `destroy` scripts keep `--production` and `--stage`:**
ADR-0050 records this as a repository-internal script grammar, not a
public command.
- **Examples and CI run the published host with a workspace override:**
`gotchas.md` and `scripts/check-cli-engine-pin*.mjs` in prisma/composer,
which enforce it.
- **Composer runs the `alchemy` installed beside `@prisma/composer`:**
ADR-0007's amendment and `docs/design/10-domains/deploy-cli.md` in
prisma/composer.

## Deferred items and their tickets

| Ticket | Item |
| --- | --- |
| TML-3520 | Release Composer 0.29.0 and pin it in the host, so
`prisma@latest` gets the pnpm fix. |
| TML-3521 | Teardown and logs have no `prisma` command. |
| TML-3522 | Two checkouts of one app still share a local Postgres
server. |
| TML-3523 | A compute emulator test is too tight on time and flakes
under load. |
| TML-3524 | Composer's examples pin an older `prisma` host than
`latest`. |
| TML-3525 | Drop the exact `effect` pin once `effect` 4 is stable or
Alchemy pins its peer. |
| TML-3526 | dependency-cruiser skips the examples' `prisma.config.ts`.
|
| TML-3527 | Edge cases in how Composer starts Alchemy. |
| TML-3528 | prisma/asks and prisma/streams still use the retired config
or command. |

Two smaller review notes are accepted without tickets. The prisma-cli
conformance check needs a new exception on each joint engine release,
which is visible when it happens. Windows edge cases are out of scope,
because Windows is documented as unsupported for local tooling.

## What this PR deletes

Every file under `projects/one-config-file/` is transient under
`drive/project/README.md`: the spec, plan, design notes, retro log,
README, and each slice's spec, plan, grounding notes, reviews and QA
transcript. None is methodology to migrate. The decisions are mapped
above. The full files stay readable in the history of prisma/orm#30536.

## The failure-mode number

The retro added its lesson to `drive/calibration/failure-modes.md` as
F39. prisma/orm#30613 had already used F39 two days earlier. This PR
renumbers ours to F42, the next free number.

Agent: saruman-38

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants