Skip to content

feat(run-engine): queue gates enforced from the message payload - #4826

Open
matt-aitken wants to merge 8 commits into
feat/queue-total-concurrency-limitfrom
feat/queue-gates-engine
Open

feat(run-engine): queue gates enforced from the message payload#4826
matt-aitken wants to merge 8 commits into
feat/queue-total-concurrency-limitfrom
feat/queue-gates-engine

Conversation

@matt-aitken

@matt-aitken matt-aitken commented Aug 29, 2026

Copy link
Copy Markdown
Member

Summary

Stacked on #4823. Adds queue gates to the run queue: a run can name up to three additional declared queues (in practice a task's anonymous inline-limit gate plus two named limits) it must also hold a concurrency slot in while it executes. The run waits in its own queue as today; each gate contributes an extra admit condition and an extra slot held until release. This is the engine mechanism behind two long-requested shapes:

  • a per-tenant cap that spans task queues (every run gated on a tenant queue keyed by tenant id, so one tenant's total across all tasks is bounded), and
  • a shared external-resource cap (every run that calls a rate-limited provider gated on one provider queue).

Gates ride the message payload as gates: [{ queue, concurrencyKey? }]. At a gate, the gate queue's concurrencyLimit applies per key value and its totalConcurrencyLimit bounds the sum across keys, exactly the same semantics a run meets in its own queue. Enforcement is gated behind RUN_ENGINE_QUEUE_GATES_ENABLED (default off). The SDK and trigger API surface that populates gates follows in a PR stacked on this one; nothing produces gated payloads yet.

Design

Admit: both dequeue scripts already decode each message's payload, so the gate check reads it directly and skips a message whose gates are full (it stays queued in place). The enqueue fast paths probe the raw payload for "gates" before decoding, so ungated messages pay a substring scan and nothing more. On admit the run is added to each gate's per-key concurrency set, and to the gate's group set when the entry is keyed, reusing the exact structures the base queue and total cap already use. Gate keys are built in Lua from the payload's own org/project/environment, so they always live in the message's hash slot.

Release: every release path (ack, nack, dead-letter, concurrency release, sweeper clear, TTL expiry, re-enqueue cleanup) removes the run from its gate sets, driven by the payload rather than the flag, so slots acquired while the flag was on always drain after it is turned off. A saturated gate self-heals with the same bounded cursor reconciliation as the total cap in #4823: a member whose message key no longer exists was terminally released by a path without the removal and is pruned, one bounded batch per interval.

Head-of-line note: a message whose gate is full blocks the messages behind it in the same (sub-)queue. That is the intended trade and the contract PR enforces the rule that makes it benign: a gate's key must be a function of the run's own concurrency key, or a constant.

Covered by tests for unkeyed and keyed gates, flag-off behavior, nack release, fast-path fallback, and leaked-member reconciliation, plus the existing run-queue suite (129 tests).

@changeset-bot

changeset-bot Bot commented Aug 29, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 8d9cc1e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds optional queue gates to run payloads, with up to three requested gate definitions plus the inline-limit gate. Adds configuration to enable gate enforcement. Updates enqueue and dequeue Lua scripts to check capacity and acquire gate slots. Updates completion, retry, dead-letter, expiry, and concurrency cleanup paths to release gate slots. Adds stale-member reconciliation and Redis-backed tests for gate and total-concurrency behavior.

Merge Risk: 🔵 Low · up to eb98f

Queue gates add optional cross-queue concurrency control. When enabled, gate holders may be charged against a queue total-concurrency budget even when that budget is otherwise disabled, which can cause unnecessary run queuing; the feature remains disabled by default.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 8 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: enforcing queue gates from run-engine message payloads.
Description check ✅ Passed The description provides a detailed summary, design explanation, scope, feature flag behavior, and testing coverage. It does not follow every template heading, such as the checklist, explicit issue cl…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/queue-gates-engine

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

Comment @coderabbitai help to get the list of available commands.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@matt-aitken
matt-aitken force-pushed the feat/queue-gates-engine branch 2 times, most recently from 1c73107 to d1de69a Compare August 29, 2026 18:18
@matt-aitken
matt-aitken force-pushed the feat/queue-gates-engine branch from d1de69a to 209641d Compare August 29, 2026 18:21
@matt-aitken
matt-aitken force-pushed the feat/queue-gates-engine branch from 209641d to f3b6b77 Compare August 31, 2026 12:02
@pkg-pr-new

pkg-pr-new Bot commented Aug 31, 2026

Copy link
Copy Markdown

Open in StackBlitz

@trigger.dev/build

npm i https://pkg.pr.new/@trigger.dev/build@2d5a654

trigger.dev

npm i https://pkg.pr.new/trigger.dev@2d5a654

@trigger.dev/core

npm i https://pkg.pr.new/@trigger.dev/core@2d5a654

@trigger.dev/python

npm i https://pkg.pr.new/@trigger.dev/python@2d5a654

@trigger.dev/react-hooks

npm i https://pkg.pr.new/@trigger.dev/react-hooks@2d5a654

@trigger.dev/redis-worker

npm i https://pkg.pr.new/@trigger.dev/redis-worker@2d5a654

@trigger.dev/rsc

npm i https://pkg.pr.new/@trigger.dev/rsc@2d5a654

@trigger.dev/schema-to-json

npm i https://pkg.pr.new/@trigger.dev/schema-to-json@2d5a654

@trigger.dev/sdk

npm i https://pkg.pr.new/@trigger.dev/sdk@2d5a654

commit: 2d5a654

@matt-aitken
matt-aitken force-pushed the feat/queue-gates-engine branch 2 times, most recently from cc8bc17 to 2a26cc5 Compare August 31, 2026 15:26
@matt-aitken
matt-aitken force-pushed the feat/queue-gates-engine branch from 8ae0f80 to 2d5a654 Compare September 6, 2026 12:20
coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@matt-aitken
matt-aitken force-pushed the feat/queue-gates-engine branch from eb98fd1 to da7dcb8 Compare September 7, 2026 00:17

@devin-ai-integration devin-ai-integration Bot 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.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 4 new potential issues.

Devin Review

Comment thread internal-packages/run-engine/src/run-queue/index.ts Outdated
Comment thread internal-packages/run-engine/src/run-queue/index.ts Outdated
Comment thread internal-packages/run-engine/src/run-queue/index.ts Outdated
Comment thread internal-packages/run-engine/src/run-queue/types.ts
A gate is another declared queue a run must also hold a concurrency slot in
while it executes, carried in the message payload as gates =
[{queue, concurrencyKey?}] (at most two). Admit paths (both dequeue scripts
and every enqueue fast path) check each gate's per-key limit and, for keyed
entries, its total limit, then acquire a slot in each gate. Every release
path removes the run from its gate sets, driven by the payload itself with a
cheap substring probe before decoding, so gateless messages pay near zero
and releases stay correct across flag toggles.

Enforcement sits behind RUN_ENGINE_QUEUE_GATES_ENABLED (default off).
Saturated gates self-heal with the same bounded message-key reconciliation
as the total cap. The SDK and trigger surface that populates gates follows
on top of this.
…r own slot

A gate without a concurrencyKey now resolves to the run's own key inside the
scripts, matching the documented semantics for every caller and for payloads
written before a fix at the contract layer. Admission also passes a gate or
total cap whose set already contains the message: an unmirrored release from
an older build leaves the run's own membership behind, and blocking on it
would deadlock the run against itself; since acquiring is an idempotent set
add, an existing member simply keeps the slot it holds. Gate names and keys
in the payload schema are bounded to 128 characters.
A member whose message still exists but is sitting in a queue zset holds no
legitimate slot: queued and in-flight are mutually exclusive on every path.
The bounded reconcile now prunes those members too (the stored payload's
queue field is the full variant key, so one ZSCORE answers it), which frees
a saturated cap even when the self-holding run's variant lies beyond the
dequeue candidate window and can never exempt itself. Covered by a test
where the leaked member belongs to a different queue entirely.
…ncurrency set

A member left behind by a release path without the mirror (an older
build during a rolling upgrade) was only pruned if its message key was
deleted or the run was re-queued. Dead-lettered runs keep their message
key and sit in no queue, so they held a combined-cap slot forever;
suspended runs held one until their terminal ack. Group and gate sets
are strict mirrors of the home queue's currentConcurrency set, so the
reconcile now prunes any member absent from it, healing every
mirror-less path within one pass.
@matt-aitken
matt-aitken force-pushed the feat/queue-gates-engine branch from da7dcb8 to 8d9cc1e Compare September 7, 2026 00:25

@devin-ai-integration devin-ai-integration Bot 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.

Devin Review found 2 new potential issues.

Devin Review

Comment thread internal-packages/run-engine/src/run-queue/index.ts
Comment thread internal-packages/run-engine/src/run-queue/index.ts
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.

1 participant