Skip to content

fix(jobs): expire overlap locks, and back off between retries - #173

Merged
ericwang401 merged 3 commits into
4.xfrom
refactor/job-lock-keys
Oct 16, 2026
Merged

ericwang401 merged 3 commits into
4.xfrom
refactor/job-lock-keys

Conversation

@ericwang401

@ericwang401 ericwang401 commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Changes

  • Locks now expire. WithoutOverlapping locks never expired. A job killed for running past its timeout left its lock held forever, and every later job of that class for that server then failed instantly. For example, a single slow force-stop broke reinstall and delete for that server until the lock was cleared from Redis by hand.
    • A new LocksPerSubject trait expires each lock 30s after the job's timeout. A job that finds the lock held waits 5s and tries again.
    • The power and password jobs' timeouts (15s and 10s) go up to 60s, above the 15s a single Proxmox request is allowed.
  • Retries wait between attempts. Jobs whose Proxmox call failed were retried immediately; we saw 7,388 attempts in 2 seconds.
    • Polling jobs now wait 3s, 10s, then 30s.
    • Three-try jobs wait 5s, then 15s.
    • Node sync jobs wait 10s, then 30s.
  • Lock keys are just the ID (separate commit). Laravel already scopes each lock key by job class, so prefixes like server.send-power-command# were redundant.

Testing

  • Unit test: checks every lock's key, expiry and wait time.
  • Real Proxmox node: ran a power cycle through Horizon (start, wait until running, force stop, wait until stopped). No jobs failed and no locks were left behind.

Notes

WithoutOverlapping already scopes its lock by the job's class (laravel-queue-overlap:<class>:<key>), and no job shares a key with another, so prefixes like "server.send-power-command#" repeated what the class name already says. Key each lock by the server, backup or ISO ID alone.
Locks: WithoutOverlapping's lock never expires by default and is only
released when the job finishes, so a job the worker killed for running
past its timeout left it held forever, and every later job of that class
for that server failed at once. SendPowerCommandJob (15s) and
UpdatePasswordJob (10s) could hit that with a single slow Proxmox call,
which is itself allowed 15s. A new LocksPerSubject trait gives each lock
an expiry of the job's timeout + 30s and a 5s release when held. Jobs
without a timeout of their own get one, since the expiry is based on it:
300s for SyncBuildJob and PurgeBackupsJob, 60s for the rest.

Retries: jobs whose Proxmox call throws were retried immediately until
their tries or retryUntil ran out -- 7,388 attempts in two seconds for a
MonitorStateJob on a missing VM. The polling jobs now back off 3s, 10s,
then 30s; the three-try jobs 5s then 15s; the node sync jobs 10s then 30s.
WaitUntilVmIsDeletedJob is left to the delete/disconnect branch, which
already changes it.

Tested: a unit test pins every lock's key, expiry and release; a power
cycle (start, wait running, kill, wait stopped) through Horizon on a real
Proxmox node, which left no locks behind.
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Contributor License Agreement

All identified human contributors have signed the current CLA.

@ericwang401
ericwang401 merged commit 1ef3bdd into 4.x Oct 16, 2026
2 checks passed
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