Skip to content

[6.x] Prevent overlapping git commit jobs#14672

Open
aerni wants to merge 2 commits into
statamic:6.xfrom
aerni:fix/unique-commit-job
Open

[6.x] Prevent overlapping git commit jobs#14672
aerni wants to merge 2 commits into
statamic:6.xfrom
aerni:fix/unique-commit-job

Conversation

@aerni
Copy link
Copy Markdown
Contributor

@aerni aerni commented May 14, 2026

Closes #11322

Issue

Concurrent CommitJobs running against the same .git/ directory cause two related failures:

  • fatal: Unable to create '.git/index.lock': File exists. (two git add / commit processes racing on git's index mutex)
  • error: failed to push some refs ... non-fast-forward (each worker pushing with a stale view of origin, so the second push fails git's atomic ref CAS)

Both root-cause to the same thing: multiple queue workers running CommitJobs concurrently against one repo.

Solution

Mark CommitJob as ShouldBeUnique so duplicate dispatches are dropped at the dispatch layer. The first dispatch wins; its git add {{ paths }} sweeps up any changes from saves that landed during the lock window. No two git processes from CommitJob ever run simultaneously.

uniqueFor is set to 120 seconds. In normal operation the lock is released the moment handle() returns; uniqueFor is only the crash-safety net for SIGKILL / OOM scenarios where Laravel's release-on-completion code never runs. 120s comfortably outlasts the default Laravel queue worker timeout (60s), so a hard crash recovers within 2 minutes.

Attribution

Coalescing changes the attribution story: a burst of saves now produces one commit, and naively that commit gets attributed to whichever user happened to dispatch first. Other users' changes are in the commit but their name isn't on it.

The second commit in this PR addresses that with a small counter pattern:

  • Git::dispatchCommit() calls Cache::increment('statamic-git-pending-saves') on every attempt (including dispatches that will be dropped).
  • CommitJob::handle() does Cache::pull(...) at the start. If the count is greater than 1, the committer is replaced with null.
  • Git's existing fallback in gitUserName() / gitUserEmail() then attributes the commit to the configured user.name / user.email (the bot account).

This is the same fallback Statamic already uses today for scheduler-driven and CLI-driven saves where there's no authenticated user, so we're extending an existing pattern, not introducing a new one.

Michael Aerni added 2 commits May 14, 2026 12:02
Mark CommitJob as ShouldBeUnique so concurrent dispatches collapse to a
single queued job. Without this, multiple workers running CommitJobs
against the same repo race on `git add` (producing `index.lock: File
exists` errors) and on `git push` (producing non-fast-forward rejections
because each worker's push uses a stale view of origin).

The unique lock TTL is 120s, comfortably outlasting the default queue
worker timeout so a hard worker crash recovers within 2 minutes.
When multiple saves are dispatched within a single CommitJob's lock
window, ShouldBeUnique drops the duplicate dispatches and the queued
job's `git add` sweeps up everyone's changes — but the queued job still
carries the first dispatcher's user. Attributing a multi-author commit
to a single user is misleading.

Track dispatch attempts in cache via Cache::increment in dispatchCommit,
then in CommitJob::handle null out the committer when the pull reveals
more than one dispatch occurred. Git's existing fallback then uses the
configured user.name / user.email (the bot account) — consistent with
how scheduler-driven saves are already attributed.
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.

Auto-commit throwing many errors

1 participant