feat(run-ops): webapp routes — friendlyId reads, cross-seam token resolution, co-location writes#4123
Conversation
|
WalkthroughThis change migrates numerous webapp routes and admin tooling from directly querying Prisma relations (run.runtimeEnvironment, run.project, waitpoint.environment) to resolving environments via a new controlPlaneResolver and runStore abstraction. Waitpoint lookups now use a resolveWaitpointThroughReadThrough helper that fans out across run-ops and control-plane replica databases. Batch task run lookups migrate to runStore methods. Presenters (waitpoint, batch list/detail) are wired with explicit run-ops replica clients and split-read flags. Extensive new tests validate cross-seam waitpoint residency, completion guard ordering, store selection, and HTTP-callback resolution across heterogeneous Postgres databases. A changelog entry documents the routing change. Changes
Sequence Diagram(s)sequenceDiagram
participant Route as Route Loader/Action
participant RunStore as runStore
participant Replica as $replica (authorization)
participant Resolver as controlPlaneResolver
Route->>RunStore: findRun(friendlyId, select ids)
Route->>Replica: project.findFirst (membership check)
Replica-->>Route: authorized project or null
Route->>Resolver: resolveAuthenticatedEnv(runtimeEnvironmentId)
Resolver-->>Route: resolved environment or null
Route-->>Route: build redirect/response using resolved environment
sequenceDiagram
participant Route as Waitpoint Route
participant ReadThrough as resolveWaitpointThroughReadThrough
participant RunOps as run-ops replica
participant ControlPlane as control-plane replica
participant Resolver as controlPlaneResolver
Route->>ReadThrough: resolve(waitpointId, environmentId)
ReadThrough->>RunOps: waitpoint.findFirst
alt not found in run-ops
ReadThrough->>ControlPlane: waitpoint.findFirst
end
ReadThrough-->>Route: waitpoint record
Route->>Resolver: resolveAuthenticatedEnv(waitpoint.environmentId)
Resolver-->>Route: environment (with parentEnvironment apiKey fallback)
Route-->>Route: verify hash / complete waitpoint
Related PRs: None identified. Suggested labels: area: webapp, area: runengine Suggested reviewers: ericallam, matt-aitken 🥕 A carrot in the codebase, resolved through the plane, 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
9b6eee8 to
94eaef6
Compare
@trigger.dev/build
trigger.dev
@trigger.dev/core
@trigger.dev/python
@trigger.dev/react-hooks
@trigger.dev/redis-worker
@trigger.dev/rsc
@trigger.dev/schema-to-json
@trigger.dev/sdk
commit: |
|
On the three maintainability nitpicks in the review summary:
No code changes for these; the security/correctness comments have been addressed in the latest commit. |
6e4c0de to
48c395b
Compare
f8b64bd to
e145839
Compare
e145839 to
6cd0085
Compare
6cd0085 to
5ccf63f
Compare
5ccf63f to
105ba1c
Compare
e7056ff to
fa76c1b
Compare
f7cf260 to
d31f2e9
Compare
d31f2e9 to
4bfd808
Compare
4bfd808 to
9197016
Compare
9197016 to
fa0d473
Compare
fa0d473 to
50463ce
Compare
15d6987 to
4862283
Compare
4862283 to
abb6c01
Compare
abb6c01 to
a4deee1
Compare
a4deee1 to
7ed8483
Compare
7ed8483 to
77236dd
Compare
d36e335 to
4713776
Compare
…ck, co-location writes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… contract Waitpoint ids are always cuid; residency is decided by co-location routing, not id-shape. Remove the removed setKsuidMintEnabled global and flip the standalone-token assertions to cuid/LEGACY. Drive the NEW/cross-seam side from a ksuid run co-locating its (cuid) token on #new instead of a ksuid token, and keep the cross-seam guard consulted unconditionally on a plain cuid token. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The public wait-token routes (complete, HTTP callback, retrieve) resolved the waitpoint with a bare read-through that defaulted its new-side client to the dedicated run-ops replica and gated on the async mint flag. A standalone wait token has a cuid id and, having no owning run, is written to the control-plane store, so under the split topology the run-ops replica does not hold it and the routes returned 404 "Waitpoint not found". Resolve these routes the same way the working waiter route does: fan out through the run-ops replica first, then the control-plane replica, so both a co-located (run-owned) waitpoint and a standalone control-plane token are found. Gate the fan-out on the URL-presence read gate rather than the mint flag, so read visibility spans both DBs whenever both are configured — including the window where both database URLs are set but the mint flag is off. The retrieve route hands the same fan-out clients and gate to ApiWaitpointPresenter. Adds a two-database testcontainer regression proving a control-plane-resident standalone token resolves via the legacy fallback under the read gate, and that the passthrough branch (gate off) misses it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment/label/title cleanup only — no product logic or test behavior changed. Removes leftover enumeration scaffolding across the PR10 files: Test A-D and Leg 1-4 case markers, (A)/(B)/(D) parenthesized markers, Step 1-3 prefixes, and --- comment framing, while preserving the behavioral prose and the describe/it titles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lit deps for waitpoint detail Route the org-membership authorization gate through the primary client on the debug, run-inspector, and idempotency-key-reset routes so it matches the cancel and replay paths and never authorizes against lagging replica state. Restore the primary-DB org fallback in the replay action's RBAC scope resolver so the scope is never resolved without an org during replica lag. Pass the full split-read deps to WaitpointPresenter on the detail route so a waitpoint resident on the new run-ops DB resolves the same way it does on the list route. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… test's db.server mock The callback route now resolves the waitpoint via resolveWaitpointThroughReadThrough, whose module reads runOpsSplitReadEnabled off ~/db.server at import. The test's vi.mock omitted that export, so the mocked module threw at import and the whole suite failed to collect. Provide it (split-on) to match the read path the route exercises; ksuid (NEW) waitpoint ids route to the runOpsNewReplica proxy pointing at the seeded container. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-token test The "control-plane-resident standalone token is found when the run-ops replica does not hold it" case relied on the ambient RUN_OPS_SPLIT_ENABLED env being on. Locally .env sets it (plus distinct TASK_RUN_* URLs) so the resolver fans out new->legacy and finds the cuid-shaped token. In CI those vars are unset, so resolveWaitpointThroughReadThrough falls back to isSplitEnabled() -> false -> single-DB passthrough, which reads only the run-ops replica (prisma17), never the control-plane legacy replica (prisma14) that holds the token. The read returned null and the assertion "expected null not to be null" failed on CI shard 3. Inject deps.splitEnabled: true (mirroring the sibling read-gate test) so the fan-out is exercised deterministically regardless of ambient env. Test-only; preserves exactly what the case verifies (legacy fallback finds a control-plane token the new replica lacks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…deps The api.v1.batches.$batchParam.results route constructed ApiBatchResultsPresenter with no read-through deps, collapsing call() to a passthrough read off the control-plane replica only. A NEW-resident (ksuid) batch on the dedicated run-ops DB was invisible to that read and the route returned 404. Wire the presenter with splitEnabled + newClient + legacyReplica, mirroring the batch-list and waitpoint token read routes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
Wires the run-ops split into the webapp routes so requests are routed correctly across the store boundary. Builds on the presenter rework from the previous PR; this layer is the route/loader/action glue that calls those presenters and the run store.
friendlyIdthrough the run store, so a run is found regardless of which store holds it. This spans the dashboard run routes, the resource routes (resources.runs.*,resources.taskruns.*cancel/debug/replay, logs download), the API batch/run routes (api.v1/api.v2/api.v3), realtime routes, sync-traces, and the admindebugRunhelper.New tests cover token resolution across the seam (
waitpointTokenResolve), the cross-seam guard on the complete route, the control-plane callback path, and theapi.v1.waitpoints.tokenssurface.Why
Part of the run-ops database split. This is PR9 of the series (PR8/9/10): it connects the split-aware read presenters (PR8) to the actual HTTP/loader surface so reads and token resolution route correctly, without yet enabling the split. The behaviour-changing activation stays isolated in PR10 on top of this.
Tests
pnpm run test --filter webappfor the affected route/resolver suites, including the new cross-seam-guard and waitpoint-token suites.Notes
Draft, stacked on #4122 (
runops/pr08-presenters). Review that first; this diff is against it.Server-change / changeset note to be added at stack-assembly time.
🤖 Generated with Claude Code