Repository navigation
feat(dev): checkouts of one app get their own local services, and a service keeps its port - #354
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The Compute emulator no longer knows apps or service addresses. A client creates an instance with free-form labels and an optional requested port; the emulator generates the id, stores the labels verbatim and never reads them. Every operation is by instance id, start and stop take a list of ids, and GET /instances lists every instance. An instance holds its port from creation until it is deleted, running or stopped, because a deployment's env and its dependents' URLs contain the port. A requested port is used when no instance holds it and it is bindable; otherwise the lowest free port from 3000. The deployment body no longer carries an address, so failure messages name the instance id. State from the app-keyed version is discarded on load. A test fails if the emulator's code names an app, address, directory or Composer, or if a label value appears in its state, files or listings outside labels. Callers in local-target and the prisma-cloud extension move in the next commit; `pnpm typecheck` fails for them at this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…the mapping lands Temporary adaptation so `prisma dev` keeps working on instance ids. The App and Deployment providers, the emulators hook, attach and the --fresh teardown find a service's instance by the labels Composer gives it on create (composer.directory, composer.app, composer.address), creating it when none exists. The per-directory mapping file replaces this lookup. Failures that name an instance id are rewritten to name the service's address, so what `prisma dev` prints is unchanged. The integration tests read the new listing route. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ator-instances.json `prisma dev` now records, in `.prisma-composer/dev/emulator-instances.json`, the app name and, per service address, the Compute instance that runs it and that instance's port. Every Composer operation on a Compute instance goes through this record: the emulators hook, the App and Deployment providers, attach (endpoints, logs, start and stop) and the --fresh teardown. Labels are written on create and never read back, so two directories running the same app get separate instances. The emulators hook reuses a recorded instance while the emulator still has it. Otherwise it creates one, requesting the remembered port, and prints one line when the emulator had to give another port. A re-created instance runs no deployment, and a warm start's converge is a no-op, so the Deployment provider's diff asks for an update when its instance does not run the recorded artifact, and the App provider's diff asks for one when its instance's URL changed. Deleting a removed service's App deletes its instance and its entry. `readEmulatorInstances(directory)` reads the record from a fresh process. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…s app's services A directory has one dev instance. When `prisma dev` runs a different app from a directory than the one its emulator-instances.json names, the emulators hook deletes the previous app's Compute instances, which hold no data, starts a new record, and prints one line naming the app whose local services were removed. Without this, every switch left the previous app's instances holding their ports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…es per directory ADR-0052 records the model: the emulators know nothing about Composer, instances are keyed by emulator-generated ids and carry labels the emulator never reads, Composer's per-directory emulator-instances.json is the only mapping from its names to instances, an instance keeps its port until deleted, a remembered port is a preference used when an instance is re-created, and switching apps in a directory removes the previous app's services. It amends ADR-0041's app-name local identity. local-dev.md describes the Compute emulator's instances and ports, the record and how each part of dev uses it, and notes that Postgres and buckets are still keyed by the app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The running-locally guide and the core-concepts skill say that two checkouts of one app no longer share services, that a service keeps its port in its directory, what dev prints when a remembered port is taken or when a different app runs from the same directory, and that local databases and buckets are still shared by app name. The front-door examples in the running-locally and getting-started guides now match what `prisma dev` prints (`dev: ready`, then `<address>: <url>`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…p unknown instances Creating an instance is not idempotent: if the emulator handled the first attempt and the connection then failed, a retry created a second instance that held its port with nothing pointing at it. The client no longer retries POST /instances; the other admin calls are idempotent and keep their retry. A bulk stop now skips ids the emulator does not have and stops the rest, so Ctrl-C stops a directory's services even if one recorded instance has vanished. A bulk start still fails on an unknown id. The state test now deploys a payload shaped like Composer's (an artifact directory under the project's dev directory, env keys named after the address) and asserts what holds: labels and payload are stored verbatim, and no route, state key, file name or emulator-generated field carries their values. Two test names that still said "app" now say instances. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
… app the record names Teardown runs before the emulators hook, so after switching apps the record still names the app run here before. Teardown deleted only the current app's instances, then removed the record, leaving the previous app's instances holding their ports with nothing pointing at them. It now deletes every instance the record names, then empties it. `reserveServicePorts` is renamed `ensureServiceInstances`, since it ensures one instance per service rather than reserving ports. New tests against a real emulator: --fresh after switching apps leaves no instance of either app; the App provider's diff asks for an update when a re-created instance got another port and for nothing when it kept its port; a deployed service that exits before it listens fails naming its address; the record-file test checks the three labels written on create. A unit test covers the instance-id-to-address rewrite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The claim that the emulator's state holds no app, address or directory outside labels was false for every real deployment: the artifact directory lies under the project and env keys are named after addresses. ADR-0052 and local-dev.md now say what holds: the emulator never interprets Composer's data, which reaches it only as label values and inside the deployment payload it stores verbatim, and no route, state key, file name or emulator-generated field carries it. local-dev.md also notes that a bulk stop skips unknown ids and that creating an instance is never retried. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…eferred items Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Comment |
|
✅ Gizmo reviewed a16380a — posted 1 inline comment(s) this pass. Open findings: 🟡 1 minor Change walkthroughThis delta extends the emulator-instance work from the rest of PR #354 with a test-only readiness-poll knob and two race tests. The compute daemon now takes The purpose is to widen the window between a child process exiting and its start noticing, so the tests can act inside the race that #353's |
commit: |
The tests in emulator-instances.test.ts start a real Compute emulator, and local dev does not support Windows: on windows-latest the emulator fails to clear a service log that is still open (EPERM, ftruncate). Run them under describe.skipIf(process.platform === 'win32'), as the CLI's dev() and log() tests already do. The two error-rewriting tests that start no emulator stay outside it and still run on Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Brings in the `listens: false` deployment property for services that never
listen on their port, such as the cron scheduler, and carries it through the
instance-keyed emulator API:
- The deployment body (`PUT /instances/<id>/deployment`) takes an optional
`listens`. The emulator stores it on the instance record, compares it when
deciding whether a deployment changed, and marks such an instance `running`
as soon as its process is spawned, on deploy and on a session-resume start.
- The emulator stays free of Composer terms: its timeout message says to
deploy the instance with `listens: false`, and the local-target rewrite that
already replaces instance ids with service addresses turns that into
"declare it with compute({ listens: false })".
- The readiness test uses createInstance, putDeployment by id, and
stopInstances and startInstances.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ompute() option
A plain emulator error that names an instance and suggests deploying it
with `listens: false` comes back naming the service by address and
suggesting `compute({ listens: false })`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Brings in the fix that a start no longer waits for an in-flight start whose process has exited, and main. The fix applies per instance: the start token's `processExited` flag is set when that instance's child exits, and `joinableStart` guards the session-resume start and the identical-deployment path of the instance-keyed emulator. The local-target manifest keeps this branch's @internal/foundation devDependency at main's new version, 0.30.0, and the lockfile follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
There was a problem hiding this comment.
New findings: 🟠 1 major · trace
Findings outside the diff
- 🟠 Major · tests packages/1-prisma-cloud/0-lowering/dev-emulators/src/compute-main.ts — New joinability rule (a process-exited start is not joined) has no test
The delta changes when concurrent requests join an in-flight start:joinableStartreturnsundefinedonce the start's child has exited, so a resume start or an identical deployment PUT arriving in the window between child exit and the doomed start's promise settling now spawns its own attempt instead of awaiting a failure. Both changed call sites (startInstance and the PUT handler at L759-L764) and the two amended local-dev.md table rows specify this rule, butsrc/__tests__/compute-readiness.test.tshas no case for it — its start-interleaving tests (e.g. "a repeated identical deployment during a redeploy waits for the new process", "two concurrent session-resume starts share one process") only cover the still-joinable cases. This is exactly the race class #353 shipped bugs in, and the window is real (exit listener fires while the start is still awaitingwaitUntilListening), so the new semantics can regress silently.
Recommended fix: Add a concurrency test tosrc/__tests__/compute-readiness.test.tsalongside the other start-interleaving cases: spawn a service that exits before listening, wait for the exit event, then issue a session-resume start (and separately an identical PUT) and assert it fails on its own with a fresh attempt's message rather than inheriting the first start's result.
Two readiness tests assert that a stopped instance's port refuses connections. Other test files' Compute emulators give ports from 3000 at the same time, and on Linux CI another file's service answered on the same port. Each instance in this file now asks for a currently free port from 3400 to 3599, a range only this file uses, and checks it got it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Brings in the compute emulator's --ready-poll-ms argument and the two tests that prove a start does not join an in-flight start whose process has exited. Ported to the instance-keyed API: each test creates an instance on this file's own ports, deploys by id, and resumes with startInstances. The session-resume test no longer awaits the first deployment before waiting for `held`, because that deployment settles only when the dead start notices its exit, after which there is nothing left to join. Both tests fail with the joinable-start check reverted (rebuilt dist) and pass with it. local-dev.md takes #353's readiness text, keeping this branch's wording that `listens` is stored with the instance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…race test The test checked twice that the first deployment failed with "exited (code 1)"; the second copy was left by porting the test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Brings in #353's own port range for the readiness tests. The test file keeps this branch's instance-keyed version, which already takes ports 3400 to 3599 through createInstanceOnOwnPort; #353's app-keyed ensureServiceOnOwnPort has no use here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Linked issue
n/a. Part of the local-dev work that adds
prisma dev url,prisma dev stopandprisma emulator list(plan in.drive/projects/emulator-commands/).Summary
Two checkouts of the same app can now run
prisma devside by side without touching each other:Before this PR, the second
prisma devtook over the first one's services. It redeployed its own build onto the same processes and ports, and a--freshin either checkout wiped both.Decision: the local emulator that runs service processes no longer knows what an app, a node or a directory is. It runs instances under ids it generates. Composer records, in each directory, which instance runs each of that directory's services. A service keeps its port until its instance is deleted. ADR-0052 records this.
Background: what runs a service locally
prisma devruns your app against local stand-ins for Prisma Cloud. The stand-in for Prisma Compute is the Compute emulator: one long-lived process per machine that starts and supervises every local service process. Until now it identified services by app name and service name (/apps/shop/services/gateway). Two checkouts of one app have the same names, so they got the same services.The emulator runs instances
The Compute emulator now holds instances. Creating one returns an id the emulator generates (
svc_plus 16 hex characters). Deploying, starting, stopping, deleting and reading logs all take that id, andGET /instanceslists them all.An instance can carry labels, strings the emulator stores and returns but never reads. Composer writes the directory, the app and the service's address on every instance it creates, so a person reading the list can tell what started each one.
A deployment still contains Composer's names: the artifact directory sits inside the project, and environment variable names include service addresses. The emulator stores these as they are and never interprets them. A test fails if any route, state key, file name or field the emulator generates contains an app, address or directory.
Composer keeps a record per directory
.prisma-composer/dev/emulator-instances.jsonstores the app's name and, for each service, the id of its instance and its port.prisma devwrites the record before it deploys:Everything else finds instances only through the record: deploying, starting and stopping services, reading logs, and
--fresh. Nothing looks instances up by label.Ports stay with a directory
An instance keeps its port from creation until it is deleted, whether its service is running or stopped. That matters because the port is built into more than the process: the service's environment contains it, and its dependents' environments contain its URL. A warm start doesn't redeploy anything, so a port that moved between sessions would leave services running with stale values.
The record remembers each port for the one case where it can change: the emulator lost an instance, for example when its state was wiped, and Composer has to create it again. Composer then asks for the same port. If something else holds it, the service gets the lowest free port from 3000, and
prisma devprints:The re-created service and its dependents are then redeployed with the new values.
Other behaviour
prisma devprints what it printed before.Testing performed
Against a real Compute emulator in a temporary
PRISMA_COMPOSER_EMULATORS_DIR, with realbunservice processes (skipped on Windows, where local dev isn't supported):--freshand app switching remove the right instances.examples/env-param: a cold start, Ctrl-C, a warm start on the same port, a deleted instance coming back, a taken port moving, and--fresh.Also
pnpm build,typecheck,lint,lint:depsandlint:casts, and the local-dev integration scripts, each run on its own.Checklist
git commit -s) per the DCO. The DCO status check will block merge if any commit is missing aSigned-off-by:trailer.feat,fix,chore,docs,refactor,test,build) — PR titles drive the auto-generated release notes.n/aif the change is doc-only / refactor with no behavioural delta).Notes for the reviewer
docs/design/10-domains/local-dev.md,docs/guides/running-locally.mdand the core-concepts skill. The guides' example ofprisma dev's output was out of date and is fixed..drive/projects/emulator-commands/deferred.md: the[dev]notice lines go straight to stdout, outside the CLI's output handling, as the existing[dev]lines already do.Alternatives considered
prisma devwhen a remembered port is taken. That breaks one checkout whenever another happens to take the port. Moving with a notice keeps it running.Agent: attila-51
🤖 Generated with Claude Code