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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d242b1602e66d5de0ad72ef85ce8a15e885248ca14a075a2f25bb60b80c0364
4
- data.tar.gz: db8f8a6aecfe00aae955775b09c7d08568653258ef56ab874839cde702725854
3
+ metadata.gz: 0a1665e70227e5e6fad1115523a94c7457a9b9b7216e10d2092c0326817bfd44
4
+ data.tar.gz: f6a1d79bf0f55a8f2756934bb58ab3aefb83ef6369ffec7c783ef8cd6dfa50b0
5
5
  SHA512:
6
- metadata.gz: aa64c4d9ff6c4085c42018ddd293b04fd8078e702306631d8b9918c917a53903b48832d561216c25f2dcae6c36cbc15f2e6587301302165b1a3fc639c4dfa59a
7
- data.tar.gz: f76393011d3be7b4002d27a5836e86ada615375130d04ae5f1ea64002fdad4cb5f2410c45e829ad726ce386c600d353208cba9d25de9e296db796f864373f1a0
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.12
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
@@ -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
- if Workhorse.perform_jobs_in_tx
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
@@ -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.12 ruby lib
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.12"
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-01-18"
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.12
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-01-18 00:00:00.000000000 Z
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.3.11
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.