Skip to content

Update validation/publication to use snapshots - #3155

Closed
bbartman wants to merge 67 commits into
masterfrom
bmb/2781-publications
Closed

Update validation/publication to use snapshots#3155
bbartman wants to merge 67 commits into
masterfrom
bmb/2781-publications

Conversation

@bbartman

@bbartman bbartman commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Ticket 2781
Closes #2781

Description:

Motivation

Publications, discovers, and evolutions authorized each operation by running recursive internal.user_roles() queries inside Postgres. The agent already maintains an in-memory authorization Snapshot (user grants, role grants, data planes) used for task and journal authorization. This PR moves catalog authorization for those operations onto that same snapshot, replacing the recursive SQL with exact in-memory checks.

Current design

  • All authorization decisions in publications::specs::resolve_live_specs, discovers, and evolutions are made by tables::UserGrant::is_authorized and tables::RoleGrant::is_authorized, evaluated against a Snapshot.
  • fetch_live_specs is now a plain fetch-by-catalog-name; the user filtering that previously happened in SQL is applied in memory afterwards (likewise live_specs::get_live_specs / get_connected_live_specs).
  • Each operation resolves the snapshot watch once and threads the same &Snapshot through every phase of an attempt: publication initialize → resolve → build, and an entire discover including across the connector RPC.

Why one Snapshot is pinned per operation

Every check within an attempt sees one consistent point-in-time view, so there are no torn decisions where one phase observes a grant another phase doesn't. It also makes the freshness contract well-defined: staleness is a property of the snapshot relative to the operation, not of whichever snapshot each phase happened to grab. Asserted by test_publication_uses_one_snapshot_across_phases and test_discover_uses_one_snapshot_across_connector_rpc.

Freshness contract

A snapshot-based denial is authoritative only if the snapshot was taken after the operation was queued; otherwise the denial may just be a grant the snapshot hasn't observed yet.

  • The reference instant is the queued row's updated_at, threaded as DraftPublication::started_at. It is durable across retry attempts, which is what lets retries converge (a per-attempt now() could never be overtaken by a snapshot).
  • Snapshot::taken_after(anchor) is the single definition of "authoritative for this instant", shared with envelope.rs and task authorization, and allows 250ms of TEMPORAL_SKEW.
  • Denial + authoritative snapshot ⇒ a real authorization error, reported to the user.
  • Denial + stale snapshot ⇒ a retryable authz_snapshot_stale error: the snapshot's revoke token is cancelled to request an early refresh, the row stays queued, and the executor sleeps and re-polls (5s backoff) until a snapshot postdating the request is observed.
  • Callers with no durable queued instant (controllers, one-shot handlers like data-plane creation) pass started_at: None and fall back to anchoring each denied spec on its own last_pub_id timestamp. Named data planes have no equivalent fallback, so their denials are terminal for those callers.

Authorization policies evaluated

Within resolve_live_specs:

  • Drafted spec — user must hold admin to the catalog name.
  • Referenced (not drafted) live spec — user must hold read.
  • Spec-to-spec — the drafted spec must be read-authorized to each reads_from source and write-authorized to each writes_to target (RoleGrant). These are always enforced, even when user checks are skipped.
  • Data planes — user must hold read to the named/default data plane and to storage-mapping data planes.
  • Ops collections — injected into the build without requiring user capability, unless drafted.
  • verify_user_authz: false (pre-authorized system publications: controllers, data-plane creation, L2 reporting) skips only the user-level checks above.

Test matrix

Layer Coverage
snapshot.rs taken_after skew boundary semantics
publications/specs.rs (sqlx) admin-on-drafted, reads_from/writes_to grants, read-on-referenced, authorized draft resolves cleanly, terminal vs. stale denial with and without started_at, spec-relative anchor fallback, data-plane and storage-mapping data-plane freshness, attenuated grants not visible, spec-to-spec staleness applies even with verify_user_authz: false
live_specs (sqlx) unfiltered fetch never stale, authorized specs included, authoritative denial dropped, stale denial retryable, staleness boundary, connected-specs request-relative staleness
user_publications.rs (integration) reschedule on stale data-plane authz, success after late grant (new and old specs), one snapshot across phases, stale-then-authoritative denial, early snapshot-refresh request
user_discovers.rs (integration) stale vs. terminal data-plane denial, late data-plane grant and registration, stale live-spec/collection authz, preservation of authorized live collections and captures after late grants, one snapshot across the connector RPC, missing data plane terminal after refresh
Fixtures authz_specs.sql, attenuated_grants.sql

Eventual consistency and deployment

  • Snapshot refresh cadence is bounded by MIN_REFRESH_INTERVAL (20s) and MAX_REFRESH_INTERVAL (5m); stale denials request an early refresh.
  • User-visible effect: a grant issued immediately before or during a publication/discover may delay it by up to roughly one refresh interval (the job retries) instead of failing it. A genuine denial fails once an authoritative snapshot is observed.
  • Agent-only change: no new migrations or configuration.
  • Out of scope: remaining internal.user_roles() call sites (directives, evolutions' own DB queries, and the ops/ admin pre-checks in create-data-plane / update-L2-reporting) are unchanged and can migrate separately.

Workflow steps:

No user-facing workflow changes. Publications, discovers, and evolutions behave as before, except that authorization denials evaluated against a stale snapshot now retry instead of failing.

Documentation links affected:

#2782 provides additional content.

Notes for reviewers:

The invariants to verify are: (1) exactly one snapshot is resolved per operation attempt and threaded through all phases; (2) the staleness anchor is durable across retries (publications.updated_at), so stale denials converge to either success or an authoritative denial; (3) spec-to-spec RoleGrant checks are never skipped, regardless of verify_user_authz; (4) stale denials are surfaced as retryable errors and never reported to users as authorization failures.

…ide of the test harness and other basic documentation related things. I did checked into the widening of security access requirements through the use of authz I was able to confirm that it is wider than before, because before the lookup was done based on prefixes.

@jgraettinger jgraettinger left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Directionally this looks right, and I don't see a problem with the AuthZ switchover. Some comments below.

Comment thread crates/control-plane-api/src/publications/mod.rs Outdated
Comment thread crates/control-plane-api/src/publications/specs.rs Outdated
Comment thread crates/control-plane-api/src/publications/specs.rs Outdated
Comment thread crates/control-plane-api/src/publications/specs.rs Outdated
@bbartman
bbartman marked this pull request as draft July 16, 2026 17:12
@bbartman
bbartman marked this pull request as ready for review July 17, 2026 14:31
@bbartman
bbartman requested a review from jgraettinger July 20, 2026 11:18
@bbartman

Copy link
Copy Markdown
Contributor Author

Remaining uses of internal.user_roles

  1. fetch_expanded_live_specs crates/control-plane-api/src/live_specs/db.rs:129
  2. resolve_specs crates/control-plane-api/src/evolutions/db.rs#L81
  3. user_has_admin_capability crates/control-plane-api/src/directives/storage_mappings.rs#L5
  4. create_data_plane crates/control-plane-api/src/server/create_data_plane.rs#L57
  5. update_l2_reporting crates/control-plane-api/src/server/update_l2_reporting.rs#L27

These will be fixed in later PRs

@jgraettinger jgraettinger left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Progress! Some comments below. In general it needs to go further in terms of pushing down the Snapshot and having it be the full basis of AuthZ checks. Directionally, we should be entirely removing authz concerns from DB queries. We may also be able to remove some cruft like the spec capabilities that attach to fetched live spec rows (check it elsewhere, using the snapshot -- spec capabilities had run alongside the spec only because that was formerly the sole channel for fetching them out, but we have snapshots now).

Comment thread crates/agent/src/discovers.rs Outdated
Comment thread crates/control-plane-api/src/server/snapshot.rs Outdated
Comment thread crates/agent/src/discovers.rs Outdated
Comment thread crates/agent/src/discovers.rs Outdated
Comment thread crates/control-plane-api/src/evolutions/mod.rs Outdated
Comment thread crates/control-plane-api/src/discovers/mod.rs Outdated
Comment thread crates/control-plane-api/src/publications/specs.rs Outdated
bbartman added 2 commits July 30, 2026 11:06
Rather than blindly re-polling every few seconds after an authorization
denial under a stale Snapshot, publications and discovers now persist the
instant an authoritative Snapshot must postdate (awaiting_snapshot_after,
in internal.tasks, so whichever agent instance dequeues the next poll
applies the same criterion) and defer re-polls - without loading,
building, or connector work - until the local Snapshot postdates it.
Deferred polls wake on a constant 20s interval
(Snapshot::STALE_RETRY_WAKE == MIN_REFRESH_INTERVAL), and deferral is
abandoned once MAX_REFRESH_INTERVAL plus two wake cycles has elapsed,
past which refreshes are failing and the attempt proceeds rather than
gating the task forever.

Publisher no longer holds a Snapshot watch: DraftPublication carries
&Snapshot, pinned once per executor poll (and supplied by controllers,
data-plane creation, and L2 reporting from their own watches), so the
deferral decision and every authorization decision observe one view.

Also hoist the three-way authorization classifier from
publications::specs onto Snapshot::resolve_authorization, returning an
Authorization enum (Authorized / Denied / Stale) with an ok_or_stale
adapter. The same policy now serves resolve_live_specs, get_live_specs,
get_connected_live_specs, and the discover data-plane precheck, which
each previously hand-rolled it.
@bbartman bbartman self-assigned this Jul 30, 2026
bbartman added 2 commits July 30, 2026 19:32
# Conflicts:
#	crates/agent/src/discovers.rs
#	crates/agent/src/publications.rs
@bbartman

Copy link
Copy Markdown
Contributor Author

Control-Plane Upgrade Test: masterbmb/2781-publications

Date: 2026-07-31
Environment: local stack flow (ports 10000–10999), data plane ops/dp/public/local-flow-cluster
Branch under test: bmb/2781-publications (aea9fc8) — snapshot-anchored authorization for publications and discovers
Baseline: master (7ae8d00)

Objective

Verify that users and grants created under a master control plane are honored
correctly by the branch agent after an in-place service swap, and that
authorization behavior for discovery and publication matches expectations — both
positive (authorized access succeeds) and negative (unauthorized access is denied).

Procedure

  1. Full teardownmise run local:stop stopped every unit and cleared runtime
    state; the next start ran a complete supabase db reset
    (verified: 0 tenants, only seeded support grants remained).
  2. Master phase — checked out master, brought the whole stack up on master
    builds, and provisioned all users/tenants there via the onboarding API:
  3. Baseline on master — captured the old agent's behavior before switching.
  4. Agent swap — checked out bmb/2781-publications, rebuilt only the agent
    (1m16s), and restarted flow-control-agent@flow. The data plane, brokers, and
    database kept running untouched. The branch agent came up cleanly against the
    master-created database.
  5. Re-ran the verification suite against the upgraded agent.

Results: master baseline vs. branch (same users, same data)

Test master agent branch agent (post-upgrade)
Positive publish (alice → acme/) works ✅ works, ~2s, no defer
Negative publish (alice → zeta/) denied in ~5s ✅ denied in ~29s after one defer cycle
Positive discover (alice, source-hello-world) works ✅ works, ~3.4s
Negative discover (carol, no data-plane read) noDataPlane in ~3s notAuthorized in ~24s
Race: grant committed 36 ms before publish (not exercised) ✅ defers once, then Success in ~27s

Log evidence (upgraded agent)

Negative publication — provisional denial, deferral, then authoritative denial:

11:38:21 ERROR control_plane_api::publications: error=authorization for zeta/tests/intrusion-test
         was evaluated against a control-plane snapshot that is not authoritative for this
         operation; please retry the operation
11:38:21 INFO  agent::publications: publication authorization snapshot is stale; rescheduling
11:38:45 INFO  agent::publications: publication finished status=JobStatus { type: BuildFailed }

Negative discover — stale defer, then the new authoritative NotAuthorized status:

11:39:32 WARN  agent::discovers: data-plane read denied under a stale snapshot
11:39:32 INFO  agent::discovers: control-plane snapshot is stale; rescheduling discover after refresh
11:39:53 WARN  agent::discovers: user is not authorized to read data plane
11:39:53 INFO  agent::discovers: finished status=NotAuthorized

Race — grant at 11:40:14.004, publish submitted at 11:40:14.040:

11:40:18 INFO  agent::publications: publication authorization snapshot is stale; rescheduling
11:40:40 INFO  agent::publications: publication finished status=JobStatus { type: Success }

Findings

  • Deploy compatibility holds. Publications and discovers queued/persisted by
    master round-trip cleanly through the branch agent (the Option<State>-as-null
    poll-state design), and grants created on master authorize immediately
    post-upgrade with no defer.
  • Behavioral deltas are the designed ones.
    • Denials now cost one ~20s defer cycle (STALE_RETRY_WAKE = MIN_REFRESH_INTERVAL)
      before becoming authoritative; master denied instantly from direct DB reads.
    • The discover status for an unauthorized data-plane read is now the honest
      notAuthorized, where master conflated it with noDataPlane.
    • A grant committed moments before an operation is picked up via defer-and-retry
      instead of failing spuriously — the core scenario this branch fixes.

State left in place (for inspection)

  • Checkout on bmb/2781-publications; stack running with the branch agent.
  • bob holds the race-test acme/ admin grant
    (user_grants.detail = 'claude race test round 2').
  • carolco/ → ops/dp/public/ read remains deleted from role_grants.

@bbartman

Copy link
Copy Markdown
Contributor Author

More claude orchestration tests.

Orchestration E2E Tests: Stale-Snapshot Deferral Feature

Date: 2026-07-31
Branch under test: bmb/2781-publications @ 9cc60786024 (merge of master included)
Rollback target: master @ 2e1b34280c9 (includes the #3279 state-compatibility stub)
Environment: local stack flow, data plane ops/dp/public/local-flow-cluster,
tenants acme/ (alice, admin) and zeta/ (bob, admin) provisioned fresh.

Environment note (incident during setup)

Between the previous session and this one, the dev catalog had been wiped: running
cargo test -p agent --lib / cargo nextest run -p agent --lib with the ambient
DATABASE_URL connects the integration tests to the live stack database, and
TestHarness::truncate_tables() (crates/agent/src/integration_tests/harness.rs)
issues delete from on live_specs, grants, tenants, data_planes, and
refresh_tokens between tests. This also explains the 62 spurious failures seen
earlier under parallel cargo test: concurrent truncations colliding in the
shared DB. Recovery: mise run local:stop && mise run local:stack (fresh DB) plus
re-provisioning tenants — about 3 minutes. Takeaway: never run the agent test
suite while relying on local-stack state.


Test 1 — Rollback with non-null defer state

Claim under test: a publication that the branch agent has deferred — with a
non-null {"awaiting_snapshot_after": …} persisted to internal.tasks — must be
decodable and resolvable by the master agent (which carries only the #3279
stub), and a subsequent re-upgrade must leave the system healthy. This is the
untested half of the deploy-compatibility matrix (master → branch was verified
previously).

What was done

  1. 14:17:15.9 — granted bob admin on acme/ (his grant had been cleared by the
    fresh provision, and the branch agent's startup snapshot predated it).
  2. 14:17:29.1 — submitted a publication as bob (acme/rollback/items collection
    • catalog test) via flowctl catalog publish, left polling in the background.
  3. First poll deferred as designed. Captured the persisted task row:
    task_type=3, inner_state = {"awaiting_snapshot_after": "2026-07-31T14:17:29.140533Z"},
    wake_at = 14:17:51.
  4. 14:17:44.9 — stopped the branch agent inside the 20s defer window, stranding
    the non-null state in the database.
  5. 14:18:02.8 — checked out master and started the agent service; its
    ExecStartPre rebuilt the master binary (1m07.7s).
  6. 14:20:06.7 — after verifying resolution, checked out the branch again and
    restarted the service (rebuild, 1m13.4s) to complete the round trip.

Outcome — PASSED

  • The master agent dequeued the overdue task 49 ms after startup
    (14:19:10.519), decoded the non-null state with zero
    deserialization/decode errors
    in its logs, ignored it per the stub design, and
    resolved the publication by direct DB authorization: status=Success, time_queued=101.4s. The catalog test ran and passed; flowctl reported
    Publish successful at 14:19:11.5.
  • The internal.tasks row was consumed and deleted; publications.job_status = {"type": "success"}.
  • After re-upgrade, the branch agent came up healthy and immediately served
    Test 2's successful publication — nothing poisoned in either direction.

Timing

Segment Duration
Grant → defer state persisted ~14s (first poll + reschedule)
Branch agent stop → master agent serving 1m25.5s (checkout + rebuild + start)
Master startup → publication resolved 0.4s
Publication end-to-end (submit → success) 101.4s (spans the swap downtime)
Re-upgrade (checkout + rebuild + restart) 1m13.4s
Whole test ≈ 4m04s

Test 2 — Revocation race

Claim under test: the deferral mechanism only protects denials; an
authorization that succeeds against the pinned snapshot is accepted without
re-checking the database. A grant revoked an instant before submission therefore
still authorizes until a fresh snapshot lands — a deliberate eventual-consistency
window whose semantics deserve pinning so future changes don't silently flip them.

What was done

Leg 1 — revoked in DB, still granted in snapshot:

  1. Bob's acme/ grant (from Test 1) was present in the branch agent's startup
    snapshot (taken 14:21:18).
  2. 14:21:49.151 — deleted bob's grant. 32 ms later submitted a publication as
    bob (acme/revoke-race/items + test).

Leg 2 — revocation reflected after refresh:
3. Forced an early snapshot refresh (a provisional-denial authorize request as
bob against a prefix he never held, anchored at "now"), confirmed two fresh
snapshot fetches in the log.
4. 14:22:16.1 — submitted a second publication as bob (acme/revoke-race2/items).

Outcome — PASSED (both legs)

  • Leg 1: Publish successful in 5.1s — the operation was authorized by
    the stale pinned snapshot and proceeded straight through (no defer, no DB
    re-check). This documents the revocation window: at most one snapshot lifetime
    (MAX_REFRESH_INTERVAL = 5 min, typically far less once anything requests a
    refresh).
  • Leg 2: denied with buildFailed in 23.1s — one defer cycle (denial under
    a snapshot predating the new publication is provisional) followed by the
    authoritative denial under the refreshed snapshot. The revocation is fully
    effective once observed.

Timing

Segment Duration
Revoke → submit gap 32 ms
Leg 1: submit → success 5.1s
Refresh trigger → confirmed fresh snapshot ~4s
Leg 2: submit → authoritative denial 23.1s
Whole test ≈ 50s

Test 3 — Restart mid-defer

Claim under test: the defer state is durable, not in-memory. The code comments
promise the state is "persisted to internal.tasks … shared with whichever agent
instance dequeues the next poll" — so an agent crash or deploy inside the 20s
defer window must not lose, duplicate, or stall the operation.

What was done

  1. 14:22:57.4 — submitted alice's always-deferring negative publication
    (zeta/intrusion, a prefix she has no grant to: first poll is guaranteed a
    provisional denial because the snapshot predates the row).
  2. 14:23:07.5 — observed the rescheduling log and captured the persisted row:
    inner_state = {"awaiting_snapshot_after": "2026-07-31T14:22:57.353089Z"},
    wake_at = 14:23:18.99.
  3. Immediately restarted the agent (same checkout, so ExecStartPre was a no-op):
    down at 14:23:07.5, serving again at 14:23:13.6back up 5.4s before the
    persisted wake time
    .

Outcome — PASSED

  • The new agent process took its startup snapshot at 14:23:13.655
    (postdating the anchor), fired the re-poll on schedule at 14:23:23.4, read the
    state persisted by the old process, and resolved the authoritative denial:
    status=BuildFailed, time_queued=26.0s, with the proper per-spec
    "User is not authorized…" draft errors surfaced to flowctl.
  • Exactly one re-poll occurred — no duplicate processing, no stall, no lost task.

Timing

Segment Duration
Submit → defer state persisted ~10s
Agent restart (stop → serving) 6.1s
Restart → resolution (at scheduled wake) 9.7s
Publication end-to-end (submit → denial) 26.0s
Whole test ≈ 28s

Summary

# Test Result Key evidence Op end-to-end Test total
1 Rollback with non-null defer state ✅ PASSED Master decoded awaiting_snapshot_after state, 0 decode errors, resolved Success 49 ms after startup 101.4s ~4m04s
2 Revocation race ✅ PASSED Revoked-32ms-before publish still succeeds (5.1s); denied (23.1s) once snapshot refreshes 5.1s / 23.1s ~50s
3 Restart mid-defer ✅ PASSED New process resolved old process's persisted defer on schedule; one re-poll, BuildFailed 26.0s ~28s

All three previously-untested claims of the feature — rollback decodability,
authorized-under-stale acceptance, and defer-state durability — are now verified
against a live stack.

System state left in place: merged branch checked out and its agent running;
bob holds no acme/ grant (revoked in Test 2); published specs
acme/rollback/* and acme/revoke-race/* exist under alice's tenant.

@bbartman

Copy link
Copy Markdown
Contributor Author

Rollback Test: In-Flight Discovery & Publication Across a Release Revert

Date: 2026-07-31
Branch under test: bmb/2781-publications @ 9cc60786024 (master merged in)
Rollback target: master @ 2e1b34280c9 (carries the #3279 state-compatibility stub)
Environment: local stack flow; tenants acme/ (alice), zeta/ (bob),
carolco/ (carol); data plane ops/dp/public/local-flow-cluster;
connector ghcr.io/estuary/source-hello-world:dev.

Claim under test

If a release running the stale-snapshot deferral feature must be rolled back to
master while discoveries and publications are in process
, every in-flight
operation still returns a result to the waiting user — none are lost, stuck,
duplicated, or crashed by the version change. Four in-flight shapes were covered
in a single rollback:

Op Type State at rollback
P1 Publication Mid-defer: branch agent had persisted non-null awaiting_snapshot_after, sleeping until an authoritative snapshot
D1 Discovery Mid-defer: same, via the data-plane-read staleness path
P2 Publication Queued, never polled: submitted while no agent was running
D2 Discovery Queued, never polled: submitted while no agent was running

P1/D1 were arranged to succeed after rollback (grants committed just before
submission, invisible to the branch agent's pinned snapshot but visible to
master's direct DB checks) — a positive resolution is the strongest form of
"the operation returned".

What was done

Arrangement — bob held no acme/ grant and carolco/ had no
ops/dp/public/ read grant; the branch agent was restarted so its snapshot was
authoritative for that grant-less world.

  1. 14:34:10.9 — committed both grants (bob → acme/ admin; carolco/
    ops/dp/public/ read) and, in the same script, launched P1
    (flowctl catalog publish, bob, acme/rb3-pub-defer/*) and D1 (PostgREST
    discovers insert as carol, carolco/rb3-defer/source-hello-world).
    The single-script choreography matters: a first attempt with separate steps
    lost the race — LISTEN/NOTIFY dequeue is so fast that both operations deferred
    and resolved under the branch agent before the stop landed. (That failed
    attempt incidentally provided live confirmation that the discover grant-race
    also defers-then-succeeds under the branch agent, in 19s.)
  2. 14:34:11.1 — both first polls ran ~100 ms after submission and deferred.
    Captured from internal.tasks:
    • publications task (task_type=3): {"awaiting_snapshot_after": "2026-07-31T14:34:11.057049Z"}, wake 14:34:31
    • discovers task (task_type=4): {"awaiting_snapshot_after": "2026-07-31T14:34:11.065559Z"}, wake 14:34:31
  3. 14:34:16.5stopped the branch agent 15 s before the scheduled wakes,
    stranding both deferred tasks.
  4. 14:34:56 / 14:35:04 — with no agent running, submitted P2 (alice,
    acme/rb2-pub-queued/*) and D2 (carol, carolco/rb2-queued/…). Verified all
    four rows pending in internal.tasks: two with non-null defer state, two with
    (null) state — the exact deploy-compat matrix.
  5. 14:35:21 — checked out master and started the agent service
    (ExecStartPre rebuild + start: 1m17.3s). Clients kept polling PostgREST
    throughout — submission and polling never require the agent.
  6. After verification, re-upgraded: checked out the branch and restarted
    (1m21.0s), then ran a sanity publication.

Outcome — PASSED

All four operations returned successfully within 3.3 seconds of the master
agent serving, with zero state-decode errors in its logs:

14:36:38.964 publication finished id=159895297882b000 time_queued=PT147.45S  Success   (P1, was mid-defer)
14:36:39.023 publication finished id=15989581b982d000 time_queued=PT102.26S  Success   (P2, queued while down)
14:36:40.128 discovers: finished  id=159895910502e000 time_queued=PT94.43S   Success   (D2, queued while down)
14:36:40.136 discovers: finished  id=159895297d02bc00 time_queued=PT147.45S  Success   (D1, was mid-defer)
  • Master decoded both non-null defer states (the 2781: Preemptively deploying states for Discovery and Publish #3279 stub's purpose),
    ignored them, and resolved each operation by its direct-SQL authorization —
    which saw the pre-submission grants and authorized.
  • Both discoveries ran the source-hello-world connector on the data plane to
    completion under master; both publications ran their catalog tests and
    committed.
  • Every waiting client received its result: two Publish successful, two
    {"type": "success"}. internal.tasks drained to zero rows of types 3/4 —
    nothing stuck, nothing duplicated.
  • Re-upgrade to the branch was clean: agent healthy, sanity publication
    succeeded in 5.1s.

Timing

Segment Duration
T0 grants → both ops submitted → both defers persisted ~350 ms (LISTEN/NOTIFY dequeue)
Defer persisted → agent stopped 5.4s (15s of margin before wake)
Agent downtime (stop → master serving) 2m22s (incl. 1m17.3s rebuild+start)
Master serving → all 4 operations resolved 3.3s
P1/D1 end-to-end (submit → success, spanning rollback) 147.5s
P2/D2 end-to-end (submit → success) 102.3s / 94.4s
Re-upgrade (checkout + rebuild + restart) 1m21.0s
Sanity publication post-re-upgrade 5.1s
Whole test (arm → verified re-upgrade) ≈ 4m36s

Findings

  • Rollback is safe for in-flight work. Both persisted-defer and
    queued-unpolled operations of both types survive a branch → master revert and
    resolve promptly; end-to-end latency is dominated entirely by agent downtime,
    not by any state-compatibility cost.
  • The queue is the contract. Because clients submit and poll exclusively via
    PostgREST, operations submitted during the outage were also picked up cleanly —
    a rollback with user traffic in progress degrades to added latency, never to
    lost or erroring operations.
  • Semantics may shift across the revert, by design. P1/D1 resolved as
    Success under master (direct DB reads see the fresh grants immediately). Had
    they stayed on the branch they would also have succeeded, one STALE_RETRY_WAKE
    later. The inverse case — an op the branch would defer then deny — resolves
    under master as an immediate denial (noDataPlane instead of notAuthorized
    for discovers), consistent with the earlier upgrade-test observations.
  • Test-harness note: arranging "stop inside the 20s defer window" requires
    single-script choreography; separate orchestration steps are too slow because
    first polls fire ~100 ms after row insert via LISTEN/NOTIFY.

System state left in place: merged branch checked out, its agent running;
bob again holds acme/ admin and carolco/ holds ops/dp/public/ read (both
detail = 'claude rollback-inflight test rb3'); published specs
acme/rb3-pub-defer/*, acme/rb2-pub-queued/*, acme/rb3-sanity/* exist.

@jshearer jshearer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Alright! I took one more pass over this from a DRY and comment-correctness and have the following feedback, then I think we're good to go to get this in early next week. Thanks a lot for doing the validation above, that's great 👍

Comment thread crates/agent/src/publications.rs
Comment thread crates/agent/src/integration_tests/user_publications.rs Outdated
Comment thread crates/agent/src/integration_tests/user_discovers.rs Outdated
Comment thread crates/agent/src/integration_tests/user_discovers.rs Outdated
Comment thread crates/control-plane-api/src/live_specs/mod.rs Outdated
Comment thread crates/agent/src/controlplane.rs Outdated
Comment thread crates/control-plane-api/src/publications/specs.rs Outdated
Comment thread crates/control-plane-api/src/publications/specs.rs Outdated
Comment thread crates/control-plane-api/src/publications/mod.rs Outdated
Comment thread crates/agent/src/discovers.rs
Comment thread crates/control-plane-api/src/live_specs/mod.rs
Comment thread crates/agent/src/integration_tests/harness.rs
…_connected_live_specs and updated tests and other impacted parts of the code as well.'
# Conflicts:
#	crates/control-plane-api/src/evolutions/mod.rs

@GregorShear GregorShear left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry for the drive by here, but i'd like to see us adopting the new authz model rather than adding more instances of legacy read/write/admin that we'll have to replace soon anyway

Comment thread crates/control-plane-api/src/server/snapshot.rs Outdated
Comment thread crates/control-plane-api/src/publications/specs.rs Outdated
for name in candidate_data_plane_names {
if !verify_user_authz
|| snapshot
.user_authorization(user_id, name, models::Capability::Read, started)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jshearer any suggestions for which capability bit to require in place of Read here? Do we need to make a new one?

@jshearer jshearer Aug 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, I think we should make a new one named DataPlaneDeploy (or something like that) and roll that into the existing Viewer bundle so the behavior of legacy read capability conferring deployment permission continues to work. We should also add it to the create_data_plane codepath like we did with ViewDataPlanePrivateNetworking.

That being said, I dont think this needs to land in this PR necessarily. do you?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I would like to move the change to the next PR if possible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Or even better one after the next one.

bbartman added a commit that referenced this pull request Aug 11, 2026
…ers on staleness

The discovers executor moves its data-plane authorization onto the Snapshot
and gains the same defer-on-stale behavior as publications, completing the
removal of internal.user_roles() from the discover path:

- The inline SQL data-plane query is replaced by Snapshot::user_authorization
  plus data_plane_by_catalog_name, distinguishing the three cases the SQL
  conflated: an authoritative denial is the new terminal
  JobStatus::NotAuthorized; a denial under a Snapshot predating the discover
  requests an early refresh and retries; and a plane genuinely absent from an
  authoritative Snapshot remains NoDataPlane, with a refresh-and-recheck
  grace for planes registered after the Snapshot was taken.
- started_at anchors staleness to the queued discover row, and
  AuthorizationSnapshotStale from live-spec reads reschedules via
  DiscoverState::awaiting_snapshot_after instead of reporting a spurious
  failure. DiscoverOutcome becomes Resolved / RetryStale.
- Harness: add_data_plane, queue_discover, discover_job_status, and
  SnapshotRefresher for refreshing from 'static connector fixtures.

After this change the stacked tree is identical to #3155.

Split 4 of 4 from #3155.
@bbartman

Copy link
Copy Markdown
Contributor Author

Note that this branch was migrated to the following PR #3341

@bbartman bbartman closed this Aug 12, 2026
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.

Update validation/publication to use snapshots

4 participants