workhorse 1.2.12 → 1.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +69 -0
- data/VERSION +1 -1
- data/lib/workhorse/active_job_extension.rb +20 -0
- data/lib/workhorse/jobs/run_active_job.rb +6 -0
- data/lib/workhorse/performer.rb +5 -1
- data/lib/workhorse/poller.rb +34 -0
- data/lib/workhorse.rb +7 -0
- data/workhorse.gemspec +4 -4
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0a1665e70227e5e6fad1115523a94c7457a9b9b7216e10d2092c0326817bfd44
|
4
|
+
data.tar.gz: f6a1d79bf0f55a8f2756934bb58ab3aefb83ef6369ffec7c783ef8cd6dfa50b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8dde03d8f2a8f5c05307787433ef308c99c34ad2ab59d7c2edcc352c0428241a48ee5d0356f4a9a35a0de7dac876d60e1c09cd3c016915929f786a387a9e75e6
|
7
|
+
data.tar.gz: b380dff403c8c10df7c2472422e462d66d40bff3796cbee09e81c63c11eafe4ba4a0d0dd361828f5a984f738fe30744b6fcea3616b6a54a9d1f7df016b21219f
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,28 @@
|
|
1
1
|
# Workhorse Changelog
|
2
2
|
|
3
|
+
## 1.2.14 - 2023-08-23
|
4
|
+
|
5
|
+
* Add documentation for transaction handling.
|
6
|
+
|
7
|
+
* Add support for skipping transactions on a per-job basis
|
8
|
+
|
9
|
+
## 1.2.13 - 2023-02-20
|
10
|
+
|
11
|
+
* Add the `config.max_global_lock_fails` setting (defaults to 10). If a
|
12
|
+
worker's poller cannot acquire the global lock, an error is logged, and if
|
13
|
+
`config.on_exception` is configured, the error is handled using this callback.
|
14
|
+
|
15
|
+
This change allows you to be aware of essentially defunct worker processes due
|
16
|
+
to a global lock that could not be obtained, for example, because of another
|
17
|
+
worker that was killed without properly releasing the lock. However, this is
|
18
|
+
an edge case because:
|
19
|
+
|
20
|
+
1. The lock is released by Workhorse in an `ensure` block.
|
21
|
+
2. At least MySQL is supposed to release global locks obtained in a connection
|
22
|
+
when that connection is closed.
|
23
|
+
|
24
|
+
Sitrox reference: #110339.
|
25
|
+
|
3
26
|
## 1.2.12 - 2023-01-18
|
4
27
|
|
5
28
|
* Call `on_exception` callback on failed `Performer` initialization (e.g. when
|
data/README.md
CHANGED
@@ -278,6 +278,57 @@ polling interval.
|
|
278
278
|
This setting is recommended for all setups and may eventually be enabled by
|
279
279
|
default.
|
280
280
|
|
281
|
+
## Transactions
|
282
|
+
|
283
|
+
By default, each job is run in an individual database transaction. An exception
|
284
|
+
to this is when performing ActiveJob jobs using `perform_now`, where no
|
285
|
+
transaction is created.
|
286
|
+
|
287
|
+
### Transaction callback
|
288
|
+
|
289
|
+
By default, transactions are created using `ActiveRecord::Base.transaction { ...
|
290
|
+
}`. You can customize this using the setting `config.tx_callback` in your
|
291
|
+
`config/initializers/workhorse.rb` (see commented out section in the generated
|
292
|
+
configuration file).
|
293
|
+
|
294
|
+
### Turning off transactions
|
295
|
+
|
296
|
+
You can turn off transaction wrapping in the following ways:
|
297
|
+
|
298
|
+
- Globally using the setting `config.perform_jobs_in_tx = false` in your
|
299
|
+
`config/initializers/workhorse.rb`. This is not recommended as running jobs
|
300
|
+
without transactions can potentially be harmful.
|
301
|
+
|
302
|
+
- On a per-job basis. This is the recommended approach for jobs that either open
|
303
|
+
up their own transaction(s) or jobs that explicitly do not need a transaction
|
304
|
+
for whatever reason.
|
305
|
+
|
306
|
+
Usage of this feature depends on whether you are dealing with an ActiveJob job
|
307
|
+
or a plain Workhorse job class.
|
308
|
+
|
309
|
+
For ActiveJob:
|
310
|
+
|
311
|
+
1. Add the following mixin to your job class (usually `ApplicationJob`):
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
class ApplicationJob
|
315
|
+
include Workhorse::ActiveJobExtension
|
316
|
+
end
|
317
|
+
```
|
318
|
+
|
319
|
+
2. Use the DSL-method `skip_tx` inside the job classes where you want to
|
320
|
+
disable transaction wrapping, e.g.:
|
321
|
+
|
322
|
+
```ruby
|
323
|
+
class MyJob < ApplicationJob
|
324
|
+
skip_tx
|
325
|
+
|
326
|
+
def perform
|
327
|
+
# Something without transaction
|
328
|
+
end
|
329
|
+
end
|
330
|
+
```
|
331
|
+
|
281
332
|
## Exception handling
|
282
333
|
|
283
334
|
Per default, exceptions occurring in a worker thread will only be visible in the
|
@@ -419,6 +470,24 @@ This means that you should always have an external *watcher* (usually a
|
|
419
470
|
cronjob), that calls the `workhorse watch` command regularly. This would
|
420
471
|
automatically restart crashed worker processes.
|
421
472
|
|
473
|
+
### Unobtainable Locks
|
474
|
+
|
475
|
+
Each Workhorse worker uses a poller to check the database for new jobs. To
|
476
|
+
ensure that no job is obtained by more than one worker, a global database lock
|
477
|
+
is used. If a worker is killed, it may happen that the lock is not properly
|
478
|
+
released, which can cause all pollers to stop working because they cannot
|
479
|
+
acquire the lock. This is an edge case, as the locks should typically be
|
480
|
+
released properly, even if a worker process is killed using `SIGKILL`.
|
481
|
+
|
482
|
+
In the event that this still happens, Workhorse takes the following steps:
|
483
|
+
|
484
|
+
- Logs when a lock could not be obtained.
|
485
|
+
- Retries acquiring the lock on the next poll.
|
486
|
+
- Calls the `on_exception` callback (if configured) after a configurable number of consecutive failures to obtain the lock.
|
487
|
+
|
488
|
+
The maximum number of consecutive failures can be configured using
|
489
|
+
`config.max_global_lock_fails`, which defaults to 10.
|
490
|
+
|
422
491
|
### Stuck queues
|
423
492
|
|
424
493
|
Jobs in named queues (non-null queues) are always run sequentially. This means
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.2.
|
1
|
+
1.2.14
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Workhorse
|
2
|
+
module ActiveJobExtension
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class_attribute :_skip_tx
|
7
|
+
self._skip_tx = false
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def skip_tx
|
12
|
+
self._skip_tx = true
|
13
|
+
end
|
14
|
+
|
15
|
+
def skip_tx?
|
16
|
+
_skip_tx
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,9 +1,15 @@
|
|
1
1
|
module Workhorse::Jobs
|
2
2
|
class RunActiveJob
|
3
|
+
attr_reader :job_data
|
4
|
+
|
3
5
|
def initialize(job_data)
|
4
6
|
@job_data = job_data
|
5
7
|
end
|
6
8
|
|
9
|
+
def job_class
|
10
|
+
@job_data['job_class'].safe_constantize
|
11
|
+
end
|
12
|
+
|
7
13
|
def perform
|
8
14
|
ActiveJob::Base.execute(@job_data)
|
9
15
|
end
|
data/lib/workhorse/performer.rb
CHANGED
@@ -61,7 +61,11 @@ module Workhorse
|
|
61
61
|
log 'Performing', :info
|
62
62
|
log "Description: #{@db_job.description}", :info unless @db_job.description.blank?
|
63
63
|
|
64
|
-
|
64
|
+
inner_job_class = deserialized_job.try(:job_class) || deserialized_job.class
|
65
|
+
skip_tx = inner_job_class.try(:skip_tx?)
|
66
|
+
log "SKIP TX: #{skip_tx.inspect}".red, :error
|
67
|
+
|
68
|
+
if Workhorse.perform_jobs_in_tx && !skip_tx
|
65
69
|
Workhorse.tx_callback.call do
|
66
70
|
deserialized_job.perform
|
67
71
|
end
|
data/lib/workhorse/poller.rb
CHANGED
@@ -15,6 +15,8 @@ module Workhorse
|
|
15
15
|
@table = Workhorse::DbJob.arel_table
|
16
16
|
@is_oracle = ActiveRecord::Base.connection.adapter_name == 'OracleEnhanced'
|
17
17
|
@instant_repoll = Concurrent::AtomicBoolean.new(false)
|
18
|
+
@global_lock_fails = 0
|
19
|
+
@max_global_lock_fails_reached = false
|
18
20
|
end
|
19
21
|
|
20
22
|
def running?
|
@@ -86,6 +88,38 @@ module Workhorse
|
|
86
88
|
success = result == 1
|
87
89
|
end
|
88
90
|
|
91
|
+
if success
|
92
|
+
@global_lock_fails = 0
|
93
|
+
@max_global_lock_fails_reached = false
|
94
|
+
else
|
95
|
+
@global_lock_fails += 1
|
96
|
+
|
97
|
+
unless @max_global_lock_fails_reached
|
98
|
+
worker.log 'Could not obtain global lock, retrying with next poll.', :warn
|
99
|
+
end
|
100
|
+
|
101
|
+
if @global_lock_fails > Workhorse.max_global_lock_fails && !@max_global_lock_fails_reached
|
102
|
+
@max_global_lock_fails_reached = true
|
103
|
+
|
104
|
+
worker.log 'Could not obtain global lock, retrying with next poll. '\
|
105
|
+
'This will be the last such message for this worker until '\
|
106
|
+
'the issue is resolved.', :warn
|
107
|
+
|
108
|
+
message = "Worker reached maximum number of consecutive times (#{Workhorse.max_global_lock_fails}) " \
|
109
|
+
"where the global lock could no be acquired within the specified timeout (#{timeout}). " \
|
110
|
+
'A worker that obtained this lock may have crashed without ending the database ' \
|
111
|
+
'connection properly. On MySQL, use "show processlist;" to see which connection(s) ' \
|
112
|
+
'is / are holding the lock for a long period of time and consider killing them using '\
|
113
|
+
"MySQL's \"kill <Id>\" command. This message will be issued only once per worker " \
|
114
|
+
"and may only be re-triggered if the error happens again *after* the lock has " \
|
115
|
+
"been solved in the meantime."
|
116
|
+
|
117
|
+
worker.log message
|
118
|
+
exception = StandardError.new(message)
|
119
|
+
Workhorse.on_exception.call(exception)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
89
123
|
return unless success
|
90
124
|
|
91
125
|
yield
|
data/lib/workhorse.rb
CHANGED
@@ -6,6 +6,7 @@ require 'uri'
|
|
6
6
|
|
7
7
|
require 'workhorse/enqueuer'
|
8
8
|
require 'workhorse/scoped_env'
|
9
|
+
require 'workhorse/active_job_extension'
|
9
10
|
|
10
11
|
module Workhorse
|
11
12
|
# Check if the available Arel version is greater or equal than 7.0.0
|
@@ -20,6 +21,12 @@ module Workhorse
|
|
20
21
|
|| fail('No performer is associated with the current thread. This method must always be called inside of a job.')
|
21
22
|
end
|
22
23
|
|
24
|
+
# A worker will log an error and, if defined, call the on_exception callback,
|
25
|
+
# if it couldn't obtain the global lock for the specified number of times in a
|
26
|
+
# row.
|
27
|
+
mattr_accessor :max_global_lock_fails
|
28
|
+
self.max_global_lock_fails = 10
|
29
|
+
|
23
30
|
mattr_accessor :tx_callback
|
24
31
|
self.tx_callback = proc do |*args, &block|
|
25
32
|
ActiveRecord::Base.transaction(*args, &block)
|
data/workhorse.gemspec
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
# stub: workhorse 1.2.
|
2
|
+
# stub: workhorse 1.2.14 ruby lib
|
3
3
|
|
4
4
|
Gem::Specification.new do |s|
|
5
5
|
s.name = "workhorse".freeze
|
6
|
-
s.version = "1.2.
|
6
|
+
s.version = "1.2.14"
|
7
7
|
|
8
8
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
9
9
|
s.require_paths = ["lib".freeze]
|
10
10
|
s.authors = ["Sitrox".freeze]
|
11
|
-
s.date = "2023-
|
12
|
-
s.files = [".github/workflows/ruby.yml".freeze, ".gitignore".freeze, ".releaser_config".freeze, ".rubocop.yml".freeze, "CHANGELOG.md".freeze, "FAQ.md".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "RUBY_VERSION".freeze, "Rakefile".freeze, "VERSION".freeze, "bin/rubocop".freeze, "lib/active_job/queue_adapters/workhorse_adapter.rb".freeze, "lib/generators/workhorse/install_generator.rb".freeze, "lib/generators/workhorse/templates/bin/workhorse.rb".freeze, "lib/generators/workhorse/templates/config/initializers/workhorse.rb".freeze, "lib/generators/workhorse/templates/create_table_jobs.rb".freeze, "lib/workhorse.rb".freeze, "lib/workhorse/daemon.rb".freeze, "lib/workhorse/daemon/shell_handler.rb".freeze, "lib/workhorse/db_job.rb".freeze, "lib/workhorse/enqueuer.rb".freeze, "lib/workhorse/jobs/cleanup_succeeded_jobs.rb".freeze, "lib/workhorse/jobs/detect_stale_jobs_job.rb".freeze, "lib/workhorse/jobs/run_active_job.rb".freeze, "lib/workhorse/jobs/run_rails_op.rb".freeze, "lib/workhorse/performer.rb".freeze, "lib/workhorse/poller.rb".freeze, "lib/workhorse/pool.rb".freeze, "lib/workhorse/scoped_env.rb".freeze, "lib/workhorse/worker.rb".freeze, "test/active_job/queue_adapters/workhorse_adapter_test.rb".freeze, "test/lib/db_schema.rb".freeze, "test/lib/jobs.rb".freeze, "test/lib/test_helper.rb".freeze, "test/workhorse/db_job_test.rb".freeze, "test/workhorse/enqueuer_test.rb".freeze, "test/workhorse/performer_test.rb".freeze, "test/workhorse/poller_test.rb".freeze, "test/workhorse/pool_test.rb".freeze, "test/workhorse/worker_test.rb".freeze, "workhorse.gemspec".freeze]
|
11
|
+
s.date = "2023-08-23"
|
12
|
+
s.files = [".github/workflows/ruby.yml".freeze, ".gitignore".freeze, ".releaser_config".freeze, ".rubocop.yml".freeze, "CHANGELOG.md".freeze, "FAQ.md".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "RUBY_VERSION".freeze, "Rakefile".freeze, "VERSION".freeze, "bin/rubocop".freeze, "lib/active_job/queue_adapters/workhorse_adapter.rb".freeze, "lib/generators/workhorse/install_generator.rb".freeze, "lib/generators/workhorse/templates/bin/workhorse.rb".freeze, "lib/generators/workhorse/templates/config/initializers/workhorse.rb".freeze, "lib/generators/workhorse/templates/create_table_jobs.rb".freeze, "lib/workhorse.rb".freeze, "lib/workhorse/active_job_extension.rb".freeze, "lib/workhorse/daemon.rb".freeze, "lib/workhorse/daemon/shell_handler.rb".freeze, "lib/workhorse/db_job.rb".freeze, "lib/workhorse/enqueuer.rb".freeze, "lib/workhorse/jobs/cleanup_succeeded_jobs.rb".freeze, "lib/workhorse/jobs/detect_stale_jobs_job.rb".freeze, "lib/workhorse/jobs/run_active_job.rb".freeze, "lib/workhorse/jobs/run_rails_op.rb".freeze, "lib/workhorse/performer.rb".freeze, "lib/workhorse/poller.rb".freeze, "lib/workhorse/pool.rb".freeze, "lib/workhorse/scoped_env.rb".freeze, "lib/workhorse/worker.rb".freeze, "test/active_job/queue_adapters/workhorse_adapter_test.rb".freeze, "test/lib/db_schema.rb".freeze, "test/lib/jobs.rb".freeze, "test/lib/test_helper.rb".freeze, "test/workhorse/db_job_test.rb".freeze, "test/workhorse/enqueuer_test.rb".freeze, "test/workhorse/performer_test.rb".freeze, "test/workhorse/poller_test.rb".freeze, "test/workhorse/pool_test.rb".freeze, "test/workhorse/worker_test.rb".freeze, "workhorse.gemspec".freeze]
|
13
13
|
s.rubygems_version = "3.0.3".freeze
|
14
14
|
s.summary = "Multi-threaded job backend with database queuing for ruby.".freeze
|
15
15
|
s.test_files = ["test/active_job/queue_adapters/workhorse_adapter_test.rb".freeze, "test/lib/db_schema.rb".freeze, "test/lib/jobs.rb".freeze, "test/lib/test_helper.rb".freeze, "test/workhorse/db_job_test.rb".freeze, "test/workhorse/enqueuer_test.rb".freeze, "test/workhorse/performer_test.rb".freeze, "test/workhorse/poller_test.rb".freeze, "test/workhorse/pool_test.rb".freeze, "test/workhorse/worker_test.rb".freeze]
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: workhorse
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.2.
|
4
|
+
version: 1.2.14
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sitrox
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-08-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -203,6 +203,7 @@ files:
|
|
203
203
|
- lib/generators/workhorse/templates/config/initializers/workhorse.rb
|
204
204
|
- lib/generators/workhorse/templates/create_table_jobs.rb
|
205
205
|
- lib/workhorse.rb
|
206
|
+
- lib/workhorse/active_job_extension.rb
|
206
207
|
- lib/workhorse/daemon.rb
|
207
208
|
- lib/workhorse/daemon/shell_handler.rb
|
208
209
|
- lib/workhorse/db_job.rb
|
@@ -245,7 +246,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
245
246
|
- !ruby/object:Gem::Version
|
246
247
|
version: '0'
|
247
248
|
requirements: []
|
248
|
-
rubygems_version: 3.
|
249
|
+
rubygems_version: 3.4.10
|
249
250
|
signing_key:
|
250
251
|
specification_version: 4
|
251
252
|
summary: Multi-threaded job backend with database queuing for ruby.
|