Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Workhorse Changelog

## Unreleased

* Fix race-condition in polling mechanism which could result in workers
trying to run a job that is not yet locked.

Sitrox reference: #128333.

## 1.3.0.rc3 - 2025-06-10

* Require Rails 7.0.0 or later
Expand Down
13 changes: 9 additions & 4 deletions lib/workhorse/poller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,9 @@ def poll

timeout = [MIN_LOCK_TIMEOUT, [MAX_LOCK_TIMEOUT, worker.polling_interval].min].max
with_global_lock timeout: timeout do
Workhorse.tx_callback.call do
job_ids = []
job_ids = []

Workhorse.tx_callback.call do
# As we are the only thread posting into the worker pool, it is safe to
# get the number of idle threads without mutex synchronization. The
# actual number of idle workers at time of posting can only be larger
Expand All @@ -230,9 +230,14 @@ def poll
worker.log 'Rolling back transaction to unlock jobs, as worker has been shut down in the meantime'
fail ActiveRecord::Rollback
end

job_ids.each { |job_id| worker.perform(job_id) }
end

# This needs to be outside the above transaction because it runs the job
# in a new thread which opens a new connection. Even though it would be
# non-blocking and thus directly conclude the block and the transaction,
# there would still be a risk that the transaction is not committed yet
# when the job starts.
job_ids.each { |job_id| worker.perform(job_id) } if running?
end
end

Expand Down
Loading