Repository navigation
fix: allow parallel targeted applies on the same pull request - #7007
Open
tweeks-reify wants to merge 4 commits into
Open
tweeks-reify wants to merge 4 commits into
tweeks-reify wants to merge 4 commits into
Conversation
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
A finishing parallel apply can clear another apply’s cancellation state, allowing queued projects to execute after atlantis cancel.
1 open issue
1 file not reviewed
server/events/mocks/mock_working_dir_locker.go: Generated file
What changed in this PR
Restores parallel targeted applies for different projects on the same pull request, addressing the regression reported in #6843 while retaining exclusive locking for plans and untargeted applies.
Changes:
- Adds shared pull locks and owner-tracked project apply claims.
- Serializes result recording, combined status publication, and automerge per pull request.
- Adds concurrency regression tests, updates mocks, and documents locking behavior.
| File | Description |
|---|---|
server/events/working_dir_locker.go |
Adds shared locks, project claims, and owner-safe release. |
server/events/working_dir_locker_test.go |
Tests lock conflicts, cancellation cleanup, and stale releases. |
server/events/project_command_runner_test.go |
Updates the test locker for the extended interface. |
server/events/mocks/mock_working_dir_locker.go |
Adds generated mocks for the new lock methods. |
server/events/apply_command_runner.go |
Enables targeted concurrency and serializes result handling. |
server/events/apply_command_runner_test.go |
Tests concurrent applies, project claims, and status consistency. |
server/events/apply_command_runner_internal_test.go |
Tests per-pull result lock serialization and cleanup. |
runatlantis.io/docs/locking.md |
Documents concurrency within a pull request. |
Review effort: Balanced
tweeks-reify
added a commit
to tweeks-reify/atlantis
that referenced
this pull request
Oct 22, 2026
Address review on runatlantis#7007: - Scope `atlantis cancel` with a mark each command takes when it arrives, instead of a pull-wide flag that every finishing command cleared. A finishing targeted apply could un-cancel a parallel one. - Give every working dir lock an owner, and start pull-level lock and claim keys with '$', which workspace names cannot contain. - Link the running job on claim conflicts, log result-lock waits, fail the status when the no-project refresh fails, and log skipped post-workflow hooks. - Correct the locking docs and record the lock order in AGENTS.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: tweeks-reify <tom.weeks@reifyhealth.com>
Since v0.46.0 every apply takes an exclusive pull-level lock before projects resolve, so targeted applies of different workspaces on one pull request reject each other. Targeted applies now take a shared pull-level lock. Plan and untargeted apply stay exclusive. Each apply claims its resolved projects until it has recorded results, and result recording, combined status and automerge run one apply at a time per pull request. Closes runatlantis#6843 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: tweeks-reify <tom.weeks@reifyhealth.com>
Address review on runatlantis#7007: - Scope `atlantis cancel` with a mark each command takes when it arrives, instead of a pull-wide flag that every finishing command cleared. A finishing targeted apply could un-cancel a parallel one. - Give every working dir lock an owner, and start pull-level lock and claim keys with '$', which workspace names cannot contain. - Link the running job on claim conflicts, log result-lock waits, fail the status when the no-project refresh fails, and log skipped post-workflow hooks. - Correct the locking docs and record the lock order in AGENTS.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: tweeks-reify <tom.weeks@reifyhealth.com>
tweeks-reify
force-pushed
the
fix/parallel-targeted-apply
branch
from
October 22, 2026 00:56
1a5e664 to
0356929
Compare
Projects that share a directory and workspace share it on disk, so claiming by project let two targeted applies run Terraform in one directory at once. Claim the workspace and directory instead, and claim a shared directory once per apply. Also pass the cancellation tracker to the PR-close executor so closing a pull request clears its cancels, leave the status alone when a no-project apply cannot re-read plan status, log skipped post-workflow hooks at warn, add tests for claim release, the cancel mark on the parallel and autoplan paths, the result lock reference count and the pull lock held while recording results, and replace timing windows in tests with a log signal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: tweeks-reify <tom.weeks@reifyhealth.com>
An apply whose VCS call hangs holds the per-pull result lock, and atlantis cancel cannot release it. Make the lock a channel semaphore so waiters warn every 2 minutes with the holder's user and targets. Closing a pull request cleared its cancels, which un-cancelled commands still running on it. Record a cancel instead, so those commands skip work in the working dirs being deleted, and remove Clear. Require the command runner's cancellation tracker, because without it every command gets mark 0 and one cancel would stop every later command. Also name the lock holder when post-workflow hooks are skipped, fix comments and docs, and test that claims stay held while results are recorded and that the result lock is released on error exits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: tweeks-reify <tom.weeks@reifyhealth.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

what
Targeted applies (
-p,-d,-w) of projects in different directories or workspaces on one pull request run in parallel again, as they did before v0.46.0. Projects that share a directory and workspace still apply one at a time, as onmaster. Plan and untargeted apply keep the exclusive pull-level lock from #6605.WorkingDirLocker.TryLockPullShared: a pull-level lock any number of targeted applies can hold together. It still conflicts with the exclusive lock (TryLockPull) and with in-flight plans, andTryLockPullnow conflicts with shared holders.WorkingDirLocker.TryLockApplyDir: once targets resolve, every apply claims the workspace and directory of each project until it has recorded results. Projects that share a directory share it on disk, so the claim leaves out the project name, and one apply covering several of them claims the directory once. A second apply into a claimed directory is rejected with the existing "workspace ... is currently locked" message, which links the running job, or says several jobs are running when more than one project in that directory is applying.ApplyCommandRunnerrecords results, publishes the combined status and automerges one apply at a time per pull request (in-process lock). A waiter logs when it starts waiting, and every 2 minutes after that it warns which apply holds the lock and for how long. The pull lock stays held through that step, because plans record results without the mutex. The silenced no-project path re-reads recorded status under the same mutex before publishing, and leaves the status alone if that read fails, since it changed nothing.atlantis cancelis scoped to the commands running when it is issued. Each command records a mark fromCancellationTrackerwhen it arrives and checks cancels against it. Before, every command cleared the pull-wide flag when it finished, so a finishing targeted apply could un-cancel a parallel one. This also stops a cancel issued while nothing was running from cancelling the next command. Closing or merging a pull request now records a cancel, so commands still running on it skip work they have not started instead of running in working dirs that were just deleted. Before, the PR-close executor was built without the tracker.Clearis gone, because dropping a pull request's entry would un-cancel its running commands. The tracker keeps one entry per cancelled or closed pull request until restart.DefaultCommandRunner.CancellationTrackeris nowvalidate:"required", because a command without a tracker gets mark 0, and with nothing clearing cancels, one cancel would then stop every later command on that pull request.atlantis cancel(UnlockByPull), a still-running command's release cannot drop a newer holder's lock.$, which workspace names cannot contain, so a workspace such as.shared-pullcannot collide with them.locking.mddescribes concurrency inside a pull request, whatatlantis canceland closing a pull request do, and the steps that share a workspace's root lock, plan preparation included.AGENTS.mdrecords the lock order.why
atlantis apply -w daily-1andatlantis apply -w daily-2now reject each other even though they share no working directory, plan file or state.project1-stagingandproject1-productionfrom the custom workflows example run Terraform in one directory at once. Releases before v0.46.0 allowed that; the claim keepsmaster's protection.UpdatePullWithResultsis read-modify-write without a transaction, so parallel applies could otherwise lose a result. Statuses published from recorded results go out one at a timeatlantis cancelcannot free them. Onmaster, cancel released the exclusive lock and the next apply finished. The lock is a channel semaphore, so waiters warn every 2 minutes with the holder's command, user and targets. Lettingatlantis cancelabandon the lock is a follow-up that can build on the same semaphore.masterhas the same race between sequential applies. Taking the mutex for those writes would not change which write lands last.atlantis cancelreleases every working dir lock, claims included, while running commands carry on, so after a cancel a second apply of a project still being applied can start. That matchesmaster, where cancel releases the exclusive lock, andlocking.mdsays so.locking.mddocuments the first three:-dor-wapply locks the workspace root (., no project name), so two of them in one workspace can collide during command build. Planning or applying an unnamed root project holds that lock for its whole run.--fail-on-pre-workflow-hook-error.approve_policiesand the locks UI discard button write pull status outside the result mutex.atlantis cancelreleases claims, so an apply that was already running can still claim its directories and apply its first execution order group after a cancel. That matchesmaster, where a running apply carries on after cancel releases the exclusive lock.master: a failed result write leaves the apply status pending; the command-build error path does not mark the command errored; a duplicate targeted apply arriving after the first one finished records the validator's rejection asapply_erroredoverapplied.atlantis cancelabandon a hung result lock; take the cancel mark in the events controller rather than inside the command goroutine, so a cancel processed before an earlier apply's goroutine starts still reaches it; give the mark its own type; pin the$reservation with a test againstvalid.ValidateWorkspaceName.tests
TestApplyCommandRunner_TargetedAppliesForDifferentWorkspacesRunInParallelreproduces the issue: two targeted applies each block inside their project apply until the other has started. It fails onmaster("apply for workspace daily-1 did not start while workspace daily-2 was applying") and passes here. It also asserts both projects are recorded as applied, that the last combined status published is 2/2 success, and that result writes never overlap. That last check uses a DB wrapper that widens the read-modify-write window Redis has.TestRunProjectCmdsWithCancellationTracker_FinishingCommandKeepsOtherCommandCancelled: two commands run on one pull request,atlantis cancelarrives, one finishes, and the other's next execution order group is still cancelled.TestRunCommentCommand_HonorsOnlyCancelsAfterCommandArrivescovers the mark taken when a comment command arrives.TestRunProjectCmdsWithCancellationTracker_ParallelGroupIgnoresCancelBeforeCommandArrivedandTestRunAutoplanCommand_HonorsOnlyCancelsAfterCommandArrivescover the mark on the parallel path and on autoplan.TestCleanUpPullCancelsRunningCommands: closing a pull request cancels commands still running on it, keeps an earlier cancel in force, and leaves later commands alone.TestApplyCommandRunner_TargetedApplyBlocksWhileProjectInSameDirectoryApplies:apply -p project1-productionis rejected whileproject1-staging, in the same directory, is applying.TestApplyCommandRunner_ApplyClaimsSharedDirectoryOnce: an apply covering both succeeds and releases the claim.TestApplyCommandRunner_HoldsPullLockAndClaimsWhileRecordingResults: while an apply records results, a plan cannot take the pull lock and another apply cannot claim its directory, targeted or not.TestPullResultLockerWarnsWhileWaiting(synctest): a waiter warns after 2 minutes with the holder's description and how long it has held the lock.TestApplyCommandRunner_NoProjectTargetedApplyWaitsForParallelResults: a silenced no-project apply waits while a parallel apply records results, then publishes a status that includes them. It waits on the lock's "waiting" log line rather than a timeout window.TestPullResultLockerKeepsEntryWhileCallersWait: a third caller still waits while the second holds the lock, so the per-pull entry is not dropped early. It runs a fixed sequence instead of a goroutine stress test, so it fails on every run when the reference count is wrong.TestApplyCommandRunner_ReleasesLocksOnErrorExits: the pull lock and every claim are released when the plan status read fails, the command build fails, a project is already claimed, or recording results fails, for targeted and untargeted applies. A further apply then finishes, so the result lock was released too. The parallel test also checks that both claims are released after success.TryLock,UnlockByPullreleasing shared locks and claims, stale releases afterUnlockByPullfor every lock kind, and workspace names that spell the old reserved keys..key marker. Every one fails. A server built withoutDefaultCommandRunner.CancellationTrackerfailsNewServervalidation.go test -race -count=10on 59 lock, cancel and apply concurrency tests: 590 runs, no races, no failures.make test,make check-fmt,make build-servicepass, exceptcmdTestExecute_ConfigFile. It fails identically on cleanmasterin my local environment, where a GitHub token in the shell overrides the config-file value.mocks/mock_working_dir_locker.goandmocks/mock_cancellation_tracker.gowere generated with pegomock. Under Go 1.27, pegomock fails locally for every interface becausegolang.org/x/toolsv0.49.0 cannot read export data version 5. I generated with a newer x/tools through a scratch-modfile.go.modis unchanged.references
--silence-no-projects) and fix: prevent concurrent PR commands across Redis-backed replicas #6825 (Redis-backed pull lease across replicas). Whichever lands second needs to account for the shared mode. The in-process locks here do not coordinate across replicas, which matches the behavior onmastertoday.architecture decision record
ADR: Not required. This is a regression fix restoring pre-v0.46 targeted-apply concurrency while keeping the #6605 protections. It does touch locking and cancel, so I can write one if maintainers prefer.
🤖 Generated with Claude Code