Repository navigation
fix(dev): give services local ports in dependency order - #350
Conversation
…ne runs prisma dev gave each service the next free port in whatever order Alchemy applied the App resources. Those resources are unlinked, so Alchemy applies them concurrently, and a fresh start of the getting-started app put quotes on 3001 and gateway on 3000 about half the time, against the docs. The emulators hook now reserves every Prisma Cloud service's port one at a time, in graph.nodes order (dependencies first, ties in declaration order), right after the Compute emulator is up. The App providers then find their port already reserved. A service that already has a port keeps it, so warm starts are unchanged. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…rder The design doc, the running-locally guide and the skill now state the rule: on a fresh start, services get ports from 3000 up in dependency order, ties in declaration order; a warm start keeps saved ports. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Two cases kept old or higher ports after --fresh. If the compute emulator was stopped, teardown skipped its delete, and the restarted daemon reloaded the saved ports. If the delete ran, get-port still held the freed ports for up to 30 seconds, so the next reservation skipped them. Teardown now starts the compute emulator before deleting the app. The emulator clears get-port's held ports when it deletes an app; its own saved allocations already keep other apps' ports out. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ame the failing service serviceAppName is now the one source for both the App displayName and the name the emulators hook reserves under, so the two cannot drift. A failed reservation names its service. The port-order test now checks that each App provider returns the port its reservation got, which does not depend on other processes on the machine, and removes its temp directories. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
… description The emulators hook now also sets up per-node emulator state before converge; its description in core and the dev command says so. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…scribe what teardown now does The parameter sat next to appName, the Composer app, and read as the same kind of thing. The teardown module comment now says compute is started first and must succeed, while Postgres and buckets are skipped when unreachable. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
|
✅ Gizmo reviewed 88aa3d3 — posted 1 inline comment(s) this pass. Open findings: 🟡 1 minor Change walkthroughThis incremental delta hardens the Teardown semantics (teardown.ts:48-54). Previously, a failed Test coverage (local-target-teardown.test.ts:60-67). A new test drives the failure path: the mocked compute The change is a narrow, self-contained delta within the extension's teardown hook (the only caller is |
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Comment |
commit: |
…fails Teardown now removes the dev state directory and the local Alchemy state, and tries the buckets delete, in a finally around the compute step. A compute failure still fails --fresh, but no longer leaves stale state on disk. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
| try { | ||
| await ensureDaemon('compute', daemonEntry('compute')); | ||
| await computeClient().deleteApp(app); | ||
| } finally { | ||
| await tolerateUnreachable(() => bucketsClient().deleteApp(app)); | ||
| removeLocalPaths([`${cwd}/${DEV_DIR}`, `${cwd}/.alchemy/state/${app}/dev`]); | ||
| } |
There was a problem hiding this comment.
🟡 Minor · error-handling — A throwing cleanup inside the finally can mask the compute delete failure
The new finally block runs removeLocalPaths(...) (and the tolerated buckets delete) after the compute step failed. removeLocalPaths uses fs.rmSync(..., { force: true }), which tolerates missing paths but still throws on real filesystem errors (EACCES, EPERM, EBUSY — plausible on Windows while a dev server holds a handle). A throw from inside a finally block replaces the exception pending from the try body, so the user would see the cleanup error instead of compute delete failed — the signal the design depends on, since packages/1-prisma-cloud/1-extensions/target/src/local-target/teardown.ts:46-47 says the compute delete must succeed because it persists port allocations. The run still stops, but the reported cause misdirects debugging of a failed --fresh. Note packages/1-prisma-cloud/1-extensions/target/src/tests/local-target-teardown.test.ts:60-67 cannot catch this because its mocked removeLocalPaths never throws.
Recommended fix
Make the cleanup step unable to mask the primary failure — either wrap the finally body in its own try { ... } catch that reports the cleanup error without discarding the pending one (e.g. console.error it, or attach it to the original error before rethrowing), or hoist the cleanup out of finally into a catch that records the compute error, runs cleanup tolerantly, then rethrows the recorded error.
Linked issue
n/a. Found by the nightly getting-started check, which follows the Composer getting-started guide from an empty directory.
Summary
The getting-started guide builds two services:
quotes, and a publicgatewaythat calls it. The guide saysquotesruns on port 3000 andgatewayon 3001, and has the reader runcurl localhost:3001to get a quote through the gateway. In about half of freshprisma dev module.tsruns, the ports come out the other way round:The
curlreachesquotes, which only accepts calls from other services, so the reader gets a 401 instead of a quote. This happened in 3 of 7 fresh runs on Composer 0.29.0.With this PR,
prisma devgives services their local ports in dependency order: a service gets its port before the services that call it. Services that don't depend on each other go in the order the module declares them. On a fresh start, each service gets the lowest free port from 3000 up, soquotesalways gets 3000 andgateway3001. A warm start keeps the ports services already have, as before.Why the ports swapped
Locally, each service becomes one
Prisma.Appresource. When Alchemy applies anApp, its provider asks the Compute emulator for the service's port, and the emulator hands a new service the lowest free port from 3000 up.The
Appresources depend only on the project, not on each other, so Alchemy applies them at the same time. Whichever request reaches the emulator first gets 3000. The gateway does depend onquotes, but on thequotesApp's identity, and that doesn't order the two port requests.Reserving the ports first, in graph order
Before Alchemy runs, the Prisma Cloud extension's
emulatorshook now reserves a port for each service, one at a time, ingraph.nodesorder. Core already sorts that list with dependencies first and ties in declaration order. The work is done by a newreserveServicePorts(container, serviceAppNames)in@internal/local-target, which calls the emulator'sensureServiceonce per service and waits for each.When Alchemy then applies the
Appresources in parallel, each provider asks for a port that already exists and gets it back unchanged. A failed reservation names the service it was for.Making
--freshstart from 3000 as wellThe new rule is that a fresh start gives the lowest free ports from 3000.
prisma dev --freshis the user's way to get a fresh start, and in two cases it didn't give those ports:--freshran shortly after the previous run. The emulator chooses ports with theget-portpackage, which holds every port it hands out for up to 30 seconds. The freed ports were still held, so the app got 3002 and 3003. The emulator now releases those held ports when it deletes an app. Other apps' ports stay safe, because the emulator's saved state already keeps them out of every allocation.One name for each
Appand its reservationA reservation only helps if it uses the same name the
Appresource uses for that service. A newserviceAppName(address)in the Prisma Cloud extension now produces both, so they can't drift apart.Docs
The rule is now in
docs/design/10-domains/local-dev.md,docs/guides/running-locally.mdand the core-concepts skill, worded the same in the guide and the skill. In core, theemulatorshook's description now says it also prepares emulator state for each node before Alchemy runs.Testing performed
local-target/src/__tests__/compute-port-order.test.ts(new; a real Compute emulator in its own directory for each test):Appprovider gets the port reserved for its service, even when the providers run at the same time in reverse order, 10 times over;The first test fails without the reservation.
target/src/__tests__/local-target-emulators.test.ts(new): for a graph shaped like the getting-started app, the hook reservesquotesthengateway, after the Compute emulator is up. When nothing orders the services, it reserves them in declaration order, and only services.target/src/__tests__/local-target-teardown.test.ts(new): teardown starts the Compute emulator before deleting the app.dev-emulators/src/__tests__/compute-multi-app.test.ts(new case): an app that is deleted and then reserved again gets back the ports it freed. Before the fix it got 3002 instead of 3000.@internal/local-target17 pass,@internal/prisma-cloud395 pass,compute-multi-appandcompute-deployment23 pass.pnpm turbo run typecheckfor@internal/local-target,@internal/prisma-cloud,@internal/dev-emulatorsand@internal/core;biome checkon the changed files;pnpm lint:deps.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
Alternatives considered:
Appresources in dependency order, so Alchemy applies them one after another. This would add ordering to the deploy graph that only local dev needs, and slow the hosted deploy for it. Services that don't depend on each other would still race.prisma devat the reservation; before, it stopped it at the deploy step. The natural place for a timeout isadminFetchin the emulator client, but every emulator call uses it, including the long-running log stream. That is a separate, broader change.Not fixed here: the emulator's port allocation isn't atomic, so two
prisma devprocesses for different apps can still be handed the same port at the same moment. And--freshstill skips the Postgres and buckets deletes when those emulators are stopped.Agent: maui-32