Skip to content

fix(queue-storage): create queue lanes as the schema owner (0.6) - #638

Merged
hardbyte merged 12 commits into
release/0.6.6from
brian/definer-lane-provisioning-0.6
Nov 6, 2026
Merged

hardbyte merged 12 commits into
release/0.6.6from
brian/definer-lane-provisioning-0.6

Conversation

@hardbyte

@hardbyte hardbyte commented Nov 6, 2026 •

Copy link
Copy Markdown
Owner

Problem

A queue-storage lane, (queue, priority, enqueue_shard), has an enqueue and a claim sequence. The head-table triggers create them through SECURITY INVOKER helpers, as whichever role first touches the lane. That role owns them, and only the roles its default privileges name get access. The first touch can come from several places:

  • an application producer enqueueing to a new queue, priority or shard;
  • the maintenance leader promoting into a shard created after queue_meta.enqueue_shards was raised, because promotion routes through shard_for_enqueue;
  • prepare_queue, run as whichever role runs the CLI.

Suppose several application roles share a runtime group role and only the schema owner and the migration role have default privileges. A lane first created by one application role is then unusable by the others, which fail with permission denied for sequence on enqueue, claim or promotion. With the prepared_lane_sequences patch, a runtime without schema CREATE can use lanes that were provisioned in advance, but it cannot create a lane at all.

Change

Definer provisioning. awa.refresh_lane_sequence_helpers now installs two functions in each queue-storage schema:

  • provision_lane_sequences(queue, priority, shard) creates a lane's two sequences.
  • provision_lane(queue, priority, shard) creates the sequences, the queue_lanes row and both head rows.

Both are SECURITY DEFINER, owned by the schema owner, with search_path = pg_catalog, pg_temp.

  • Validation. Queue, priority and shard must not be null, and the shard must be 0–63; anything else raises 22023. Queue and priority get no further checks, because queue storage accepts any queue name, including an empty one, and any smallint priority, and the definer must not reject a lane the invoker path accepts.
  • Locking. Both take the lane advisory lock that ensure_lane_inserts already takes, {schema}:{queue}:{priority}:{shard}, before writing. The lock is re-entrant for the Rust caller, which holds it already, and keeps the existing lane lock order (fix(insert): take enqueue lane locks in one order across batch inserts #625).
  • Idempotence. Existing sequences and rows are left alone. Head rows are inserted only when missing, because the head triggers fire before ON CONFLICT.
  • EXECUTE. The provisioners explicitly grant EXECUTE to PUBLIC. Schema USAGE gates access to these functions.
  • Ownership. When the migrating role is not the schema owner but can SET ROLE to it, the refresh reassigns both functions to the owner. Otherwise it warns.

The three lane helpers (ensure_lane_sequences and the two head-sync trigger functions) create a missing sequence through provision_lane_sequences when the caller may execute it. Otherwise they keep the v049 behaviour: create the sequence with schema CREATE, or raise the provisioning hint. So every path creates owner-owned sequences once the helpers are refreshed, including N-1 binaries and SQL callers of insert_job_compat.

Grant model

A new lane sequence gets privileges mirrored from the head table it backs: queue_enqueue_seq_* from queue_enqueue_heads, queue_claim_seq_* from queue_claim_heads. A grantee with INSERT or UPDATE on the table gets USAGE, SELECT, UPDATE; one with only SELECT gets SELECT. The grants are explicit and do not depend on default privileges.

AWA has no configured runtime-role name, and PUBLIC would let any role in the database move cursors: sequence privilege checks do not need schema USAGE when the sequence is addressed by OID. Mirroring the head table captures exactly the roles that may already advance a lane, because the sequence is that row's cursor in another form. The documented runtime grants therefore work unchanged.

Repair and check

  • awa.lane_sequence_problems(schema [, sequence]) is read-only and executable by PUBLIC. For each lane sequence that is not owned by the schema owner or lacks a mirrored grant, it returns:
    • the current and expected owner;
    • the GRANT statements to run as the owner, and the ALTER SEQUENCE ... OWNER TO statement;
    • repair_roles: the login roles, superusers aside, that hold the current owner's privileges and can SET ROLE to the schema owner (MEMBER before PG16), so can run both.
  • awa.repair_lane_sequence(schema, sequence) repairs one sequence. It is SECURITY INVOKER, revoked from PUBLIC, and has lock_timeout = 300ms in its definition.
    • It reassigns the sequence only when pg_has_role shows the caller can act as the current owner and SET ROLE to the schema owner, and adds grants when the caller can act as the owner. It catches insufficient_privilege per statement.
    • The work runs in a subtransaction. On 55P03 it rolls back, leaving the sequence unchanged, and returns busy.
    • It returns repaired, owner_unchanged (grants complete), unrepaired or busy, with the statements still to run and repair_roles.
  • The schema patch definer_lane_provisioning, which is v053 on main, refreshes the helpers in every installed queue-storage schema and does not reassign anything. Reassigning takes the sequence's ACCESS EXCLUSIVE lock until the transaction ends, and the patch is one transaction, so in-transaction repair would stall each repaired lane until the whole upgrade committed. The probe is satisfied once the definitions are installed, so leftover sequences never keep the patch pending and awa migrate keeps exiting 0.
  • Once the patch commits, awa migrate repairs each lane sequence with its own statement, so its own transaction. No ACCESS EXCLUSIVE lock outlives the one sequence's repair, and none waits more than 300 ms.
    • Each sequence is tried as the effective role, then, if the effective role could not repair it and differs from the login role, as the login role (SET LOCAL ROLE NONE). The documented PGOPTIONS='-c role=awa_owner' migration therefore reassigns the migration role's sequences: as awa_owner it cannot act as the migration role, but as itself it can.
    • It runs only on the run that applies the patch. Leftovers are logged as warnings grouped by schema, owner and roles, each with its next step: "busy, rerun awa storage lane-sequences --repair", "rerun as one of these roles", or, when no role can, the grant that would make the migration role able to. The run's MigrationReport also carries every result in lane_sequences.
  • New awa storage lane-sequences [--schema S] [--repair]. Without --repair it is read-only. It prints JSON, then the same next steps, and exits 2 while problems remain. --repair uses the same per-sequence path as awa migrate. External runners, which apply only the SQL, run it as the migration role after the migration; the docs also give a psql \gexec loop that repairs one sequence per transaction.
  • The docs now state the rule: run the repair as the migration role itself, not SET ROLE to the owner, because the owner holds neither the migration role's nor an app role's privileges.

Rollout

  • Binary before patch. ensure_lane_inserts folds a probe into the lane-lock round trip: has_function_privilege(to_regprocedure('{schema}.provision_lane(...)'), 'EXECUTE'). When the function is missing or not executable, it runs the existing statements unchanged. Only a positive answer is cached on the store. Lane creation is rare, so an absent function is probed again on the next new lane.
  • Function gone after use. The call runs inside a savepoint. On 42883, or 42501 when the definer lacks a privilege, the store rolls back to the savepoint, drops the cached answer, logs once, and writes the rows itself in the same transaction. The enqueue does not fail.
  • Patch before binary. Released 0.6 binaries call the same helper signatures with the same cursor semantics, and get owner-owned sequences.
  • Migration live load. Function replacement only. The repair afterwards locks one sequence at a time, for at most 300 ms of waiting plus that sequence's own work.

Validation

awa/tests/lane_ownership_roles_test.rs builds a non-superuser cluster per test:

  • a schema-owner role;
  • a migration role that is a member of it, running migrations as the owner;
  • three application roles in a runtime group;
  • default privileges for the owner and the migration role only.

Scenarios:

  • First touch by an app role. A creates a fresh lane, B enqueues, claims and completes, and C, as leader, promotes scheduled jobs into two new lanes over two shards. Run with and without runtime schema CREATE.
  • enqueue_shards raised live. The leader creates shard 1 first, then A and B produce over both shards.
  • prepare_queue as an app role.
  • Mixed fleet. One role without EXECUTE on provision_lane takes the old path alongside the new path. Then provision_lane is dropped after a store has cached it.
  • Binary before the patch. The schema is restored to its 0.6.13 shape from a fixture of the 0.6.13 helpers, and the 0.6.13 behaviour is kept.
  • Concurrent first touch. A holds a lane's creation open while B waits on the lane lock, then 20 unordered races.
  • Migration repair.
    • Lanes owned by an app role, by the migration role, and by the owner without grants.
    • awa migrate runs as the migration role with role set to the owner, as the docs describe. It adds the owner's grants as the owner, reassigns the migration role's sequences as the login role, and reports the app role's with no repair_roles and a next step naming the grant.
    • Re-running as the migration role leaves the app role's sequences unrepaired. A delegate that can act as the app role but not as the owner adds the grants (owner_unchanged).
    • Once the migration role is also a member of the app role, the check lists it in repair_roles and a repair reassigns everything.
  • Held lane. An app role holds an enqueue transaction open on one lane's sequence while awa migrate runs.
    • The migration completes, the other sequences are repaired, and only the held one keeps its owner. The report marks it busy.
    • A repair while the transaction is open returns it as busy, with a "rerun --repair" next step. After the transaction ends, a repair finishes it.
    • With the lock_timeout removed from repair_lane_sequence, this test times out: the migration waits on the lock indefinitely.

Fail-before, on release/0.6.6 without this change (subset that compiles there):

PG17 PG18
before 0/5 pass: permission denied for sequence (with CREATE), has not been provisioned (without) 0/5 pass
after 9/9 pass 9/9 pass

Existing suites updated where behaviour changed on purpose:

  • least_privilege_runtime_test now expects a runtime without CREATE to create an unprepared lane, owned by the schema owner.
  • The test that needs a runtime-owned lane revokes EXECUTE on the provisioner to get one.
  • migrate_patch_cli_test's psql helper tolerates psql closing stdin after ON_ERROR_STOP, which a larger pending bundle now reaches before the whole script is written.

Notes

  • main counterpart: fix(queue-storage): create queue lanes as the schema owner (v053) #637 (migration v053).
  • The v023 definitions change in place, as for prepared_lane_sequences. External runners pick them up through the repeatable R__definer_lane_provisioning.sql.
  • Operator steps: docs/upgrade-0.6.13-to-0.6.14.md, and "Lane sequences" in docs/security.md.

Independent review and validation

Includes the small #651 point-read fix. After a priority or queue move crossed a ready-ring wrap, retained tombstoned source rows could make get_job return old fields even after the batch succeeded. The available-candidate query now excludes full-identity tombstones (a 13-line SQL change), with priority and queue ring-wrap regressions. Both regressions fail before the fix and pass after it on PostgreSQL 17 and 18; existing aged-read and batch cases also pass. Required formatting, Clippy across all targets/features, and workspace build checks pass for the prepared source.

Full CI passed all 29 jobs for 8804c72fea9a821f4d25c21723a4f3221a5c27d3.

The current head b5016b7db3e5f0c2294af840557ebd990803d17d only reconciles ancestry with the reviewed #643. Its tree e1ece69997cc4a95de6b1657c8a79d1e08389e6a is byte-for-byte identical to 8804c72fea9a821f4d25c21723a4f3221a5c27d3, which passed the full CI above.

A lane's enqueue and claim sequences were created by whichever role first
touched the lane, through SECURITY INVOKER helpers, so only that role and
the roles its default privileges named could use them. Another runtime
role then failed with "permission denied for sequence" on enqueue, claim
or promotion.

Each queue-storage schema now has two SECURITY DEFINER functions owned by
the schema owner, provision_lane and provision_lane_sequences. The lane
helpers create a missing sequence through them, and each new sequence gets
the privileges of the head table it backs. ensure_lane_inserts calls
provision_lane when it exists and is executable, inside a savepoint, and
otherwise writes the lane rows as before.

The definer_lane_provisioning schema patch refreshes the helpers and
repairs existing lane sequences one at a time, reassigning and granting
only what the applying role may and reporting the rest.
awa.lane_sequence_problems and the new `awa storage lane-sequences` command
list what remains with the statements to run; awa.repair_lane_sequences and
`--repair` rerun the repair.
@coderabbitai

coderabbitai Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 19eadb10-6767-4aac-b673-c4ea59368ed0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Share

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-06T00:30:24.517964Z 8a8d653 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8a8d6532dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +339 to +343
AND ((SELECT count(DISTINCT p.proname) FROM pg_proc AS p \
WHERE p.pronamespace = n.oid \
AND p.proname IN ('provision_lane', 'provision_lane_sequences') \
AND p.prosecdef \
AND oidvectortypes(p.proargtypes) = 'text, smallint, smallint') < 2 \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the patch pending until provisioners have the schema owner

When the first migration is run by a role that can create these functions but cannot SET ROLE to the schema owner, refresh_lane_sequence_helpers only warns and leaves both provisioners owned by the migrating role. This probe nevertheless considers the patch applied because it checks only the signatures and prosecdef; a later awa migrate under the correct owner therefore never refreshes them, despite the warning explicitly recommending that rerun. Every subsequently provisioned sequence remains owned by the wrong role and is continually reported for repair, so include proowner = n.nspowner for both functions in the pending condition.

Useful? React with 👍 / 👎.

…vision_lane

Queue storage takes any queue name, including an empty one, and any smallint priority, so the provisioning functions only reject a null argument or a shard outside 0-63. The released-binary rehearsal now expects a 0.6.9 producer without CREATE to create a new lane, owned by the schema owner.
Once a handler returned, the executor removed the attempt from the
heartbeat set and finalized it in a detached task that Client::shutdown
did not wait for. When the last handler returned as the drain ended,
shutdown cancelled the completion batcher before that finalize reached
it, and the direct-finalize fallback raced the process exit. The attempt
stayed open with no heartbeat, so stale-heartbeat rescue ran the
finished job again after heartbeat_staleness.

Track finalizers in a TaskTracker and wait for them, within the drain
timeout, before stopping the completion batcher, heartbeat and
maintenance services. This is the shutdown part of #521 on main.
…ded lock wait

The definer_lane_provisioning patch reassigned lane sequences inside the
patch transaction. ALTER SEQUENCE ... OWNER takes the sequence's ACCESS
EXCLUSIVE lock until commit, with no lock_timeout, so an open enqueue on one
lane could stall the patch and every repaired lane waited for it to commit.

The patch now only refreshes the helpers. After it commits, awa migrate
repairs each lane sequence with its own statement through
awa.repair_lane_sequence(schema, sequence), which has lock_timeout = 300ms;
on 55P03 the sequence is left unchanged and reported as busy. The results
are in MigrationReport::lane_sequences. `awa storage lane-sequences --repair`
uses the same path, and the set-returning awa.repair_lane_sequences(schema)
is gone.

Reassigning needs the current owner's privileges, which the schema owner
lacks for sequences the migration role or an app role created. A migration
role connected with role set to the owner therefore could not reassign its
own sequences. The repair now retries such a sequence as the login role,
lane_sequence_problems reports repair_roles (the login roles that can repair
each sequence), and leftovers are reported with the next step to take. The
docs state which role to run the repair as.
@hardbyte hardbyte added the full-ci Run the full CI matrix (Python build+test, E2E) on this PR label Nov 6, 2026
Preserve the exact tested #638 tree while incorporating the late #643 merge
of the identical live-ready point-read fix. Resolve CHANGELOG with #638 content.
@hardbyte
hardbyte merged commit 22be9ae into release/0.6.6 Nov 6, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

full-ci Run the full CI matrix (Python build+test, E2E) on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant