Skip to content

fix(webapp): keep paused environments paused when concurrency limits are pushed - #4624

Closed
claude[bot] wants to merge 1 commit into
mainfrom
fix/paused-env-concurrency-limit-clamp
Closed

fix(webapp): keep paused environments paused when concurrency limits are pushed#4624
claude[bot] wants to merge 1 commit into
mainfrom
fix/paused-env-concurrency-limit-clamp

Conversation

@claude

@claude claude Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Requested by Matt Aitken · Slack thread

Before: you pause an environment, then a deploy lands (or a background worker is created, or an admin changes the concurrency/burst-factor). The environment starts picking up runs again even though the dashboard still shows it as paused.

After: a paused environment stays paused until it is resumed, no matter what else pushes its concurrency limit.

Pausing an environment sets paused in the database and writes a 0 env concurrency limit into the run queue — the 0 is the only thing that actually stops dequeueing. Any caller that pushed the limit without an explicit value (finalizeDeployment, createBackgroundWorker, the two admin environment routes) rewrote the real limit and silently un-paused the environment.

✅ Checklist

  • I have followed every step in the contributing guide
  • The PR title follows the convention.
  • I ran and tested the code works

Testing

apps/webapp/test/pauseEnvironment.server.test.ts gains two containerTest cases that wire a real RunEngine (real Redis) in place of the stubbed app singleton and assert the actual run-queue env limit:

  • pause a PRODUCTION env → limit is 0 → run the real FinalizeDeploymentService → limit is still 0, plus a control on a running env in the same test proving that deploy path really does push the limit (so the 0 can't just mean "nothing happened").
  • pause → resume → the real limit is restored, so the clamp can't regress resuming.

Both cases fail on main (expected 17 to be +0 and expected +0 to be 17) and pass with this change. pnpm run typecheck --filter webapp is clean.


Changelog

Fix paused environments starting to run work again after a deploy.


How

The clamp lives in the shared updateEnvConcurrencyLimits helper in apps/webapp/app/v3/runQueue.server.ts, so every present and future caller is covered: when no explicit limit is passed and the environment is paused, 0 is written instead of the stored maximum. An explicitly-passed limit still wins, which is what pausing itself relies on. The resume path now passes the post-update environment state (its in-memory copy was read before the un-pause and would otherwise be clamped back to 0), and the helper no longer mutates the caller's environment object — that aliasing made a pause followed by a resume on the same object write 0 twice. The existing !paused guards in allocateConcurrency and the queue-level guard in createBackgroundWorker are left in place as defence in depth, and queue-level TaskQueue.paused behaviour is untouched.

…are pushed

`updateEnvConcurrencyLimits` is the only enforcement of an environment pause: pausing
writes a 0 env concurrency limit to the run queue, which is what stops dequeueing.
Callers that push the limit without an explicit value (finalizing a deployment,
creating a background worker, the admin concurrency/burst-factor routes) rewrote the
real limit, silently resuming an environment the dashboard still showed as paused.

Clamp the pushed limit to 0 when the environment is paused and no explicit limit is
given, so every caller is covered. An explicit limit still wins. Resuming now passes
the post-update state, and the helper no longer mutates the caller's environment
object (which made a pause + resume on the same object write 0 twice).

Co-Authored-By: Claude <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 14, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ee3c358

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

@github-actions

Copy link
Copy Markdown
Contributor

Thanks for your contribution! We require all external PRs to be opened in draft status first so you can address CodeRabbit review comments and ensure CI passes before requesting a review. Please re-open this PR as a draft. See CONTRIBUTING.md for details.

@github-actions github-actions Bot closed this Aug 14, 2026

@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 1 potential issue.

Open in Devin Review

Comment on lines +9 to +17
// A paused env is only enforced by a 0 limit in the RunQueue, so a push without an explicit
// limit has to stay 0 — otherwise it silently resumes an env the dashboard still shows as paused.
const limit =
maximumConcurrencyLimit ?? (environment.paused ? 0 : environment.maximumConcurrencyLimit);

await engine.runQueue.updateEnvConcurrencyLimits(updatedEnvironment);
await engine.runQueue.updateEnvConcurrencyLimits({
...environment,
maximumConcurrencyLimit: limit,
});

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.

🟡 An environment that was just resumed can be silently stopped from picking up work

The concurrency limit written into the queue is now forced to zero whenever the caller's copy of the environment says it is paused (environment.paused ? 0 : ... at apps/webapp/app/v3/runQueue.server.ts:11-12), and that copy is read earlier in the request (and from a read replica), so a deploy that started just before a resume can overwrite the restored limit with zero and leave a running environment unable to start any work.
Impact: An environment can appear active in the dashboard yet process nothing until someone pauses and resumes it again, or another concurrency update happens.

How a stale paused flag turns into a permanent zero limit

Deploy/worker-creation callers pass the environment object obtained at authentication time: finalizeDeployment.server.ts:126 uses authenticatedEnv, createBackgroundWorker.server.ts:241 uses environment. Those objects come from findEnvironmentByApiKey, which reads via $replica (apps/webapp/app/models/runtimeEnvironment.server.ts:275-289), so paused can be stale both because of replica lag and because the row can change during the request.

Sequence: (1) deploy request authenticates while the env is paused → paused: true captured; (2) user resumes → PauseEnvironmentService writes paused: false and pushes the real limit (apps/webapp/app/v3/services/pauseEnvironment.server.ts:123); (3) the in-flight deploy reaches updateEnvConcurrencyLimits(authenticatedEnv) and the new clamp writes 0. Nothing re-pushes the env limit afterwards, and dequeueing is gated only on that Redis value (internal-packages/run-engine/src/run-queue/index.ts:467-473), so the env stalls with paused: false in the database.

Before this change an argument-less push always wrote the stored limit, so this failure mode did not exist.

Prompt for agents
In apps/webapp/app/v3/runQueue.server.ts, updateEnvConcurrencyLimits now clamps the pushed env concurrency limit to 0 based on the `paused` flag of the environment object handed in by the caller. For the argument-less callers (finalizeDeployment.server.ts, createBackgroundWorker.server.ts, the admin environment routes) that object is captured at request authentication time and is read from the read replica, so `paused` can be stale. If the environment is resumed while such a request is in flight, the later argument-less push writes 0 over the restored limit and the environment stops dequeuing even though the database says paused: false, with nothing to restore it. Consider resolving the current paused state authoritatively inside the helper (e.g. a small primary-DB read of `runtimeEnvironment.paused` for the env id, or accepting an explicit `paused` argument that callers derive from a fresh row) so the clamp cannot be driven by a stale copy. Keep the explicit-limit behaviour (pause writes 0) unchanged.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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