workhorse 1.0.0.beta0 → 1.1.0
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/.travis.yml +1 -1
- data/CHANGELOG.md +37 -0
- data/README.md +7 -3
- data/VERSION +1 -1
- data/lib/generators/workhorse/templates/create_table_jobs.rb +2 -0
- data/lib/workhorse.rb +14 -2
- data/lib/workhorse/daemon.rb +8 -6
- data/lib/workhorse/db_job.rb +1 -1
- data/lib/workhorse/enqueuer.rb +2 -1
- data/lib/workhorse/jobs/detect_stale_jobs_job.rb +2 -2
- data/lib/workhorse/performer.rb +2 -2
- data/lib/workhorse/poller.rb +9 -5
- data/lib/workhorse/worker.rb +3 -3
- data/test/lib/db_schema.rb +2 -0
- data/test/workhorse/enqueuer_test.rb +9 -0
- data/test/workhorse/poller_test.rb +3 -1
- data/workhorse.gemspec +4 -4
- metadata +11 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d0994338972770f1cb6f44db74a01c54c7ebdb9aa88afd1e9938a1565997edc8
|
4
|
+
data.tar.gz: f48f3df959cca6b69a84f77a501989b6430cc04582e56345a7441af4258f62c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2631da0ac85ffb054b9705cee07174a56c56d4b773fcd25b341ece72a6e92244eb10fb88f7748dfd905fb5d01f8fd17b64425b1d479b71b40ca850af607d6581
|
7
|
+
data.tar.gz: a4ac3e1f039fc3884e2ef0a066ff2d152b351d87926139a3098fba81ab7bedb8dd80c783e57ea0de79a6187296feee1aea73949207d0f8631af9b4415ab8cb68
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,42 @@
|
|
1
1
|
# Workhorse Changelog
|
2
2
|
|
3
|
+
## 1.1.0 - 2020-12-24
|
4
|
+
|
5
|
+
* Add `description` column to `DbJob`.
|
6
|
+
|
7
|
+
If you're upgrading from a previous version, add the `description` column
|
8
|
+
to your `DbJob` table, e.g. with such a migration:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
class AddDescriptionToWorkhorseDbJobs < ActiveRecord::Migration[6.0]
|
12
|
+
def change
|
13
|
+
add_column :db_jobs, :description, :string, after: :perform_at, null: true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
```
|
17
|
+
|
18
|
+
## 1.0.1 - 2020-12-15
|
19
|
+
|
20
|
+
* Fix handling of empty pid files
|
21
|
+
|
22
|
+
## 1.0.0 - 2020-09-21
|
23
|
+
|
24
|
+
* Stable release, identical to 1.0.0.beta2 but now extensively battle-tested
|
25
|
+
|
26
|
+
## 1.0.0.beta2 - 2020-08-27
|
27
|
+
|
28
|
+
* Add option `config.silence_poller_exceptions` (default `false`)
|
29
|
+
|
30
|
+
* Add option `config.silence_watcher` (default `false`)
|
31
|
+
|
32
|
+
## 1.0.0.beta1 - 2020-08-20
|
33
|
+
|
34
|
+
This is a stability release that is still experimental and has to be tested in
|
35
|
+
battle before it can be considered stable.
|
36
|
+
|
37
|
+
* Stop passing ActiveRecord job objects between polling and worker threads to
|
38
|
+
avoid AR race conditions. Now only IDs are passed between threads.
|
39
|
+
|
3
40
|
## 1.0.0.beta0 - 2020-08-19
|
4
41
|
|
5
42
|
This is a stability release that is still experimental and has to be tested in
|
data/README.md
CHANGED
@@ -81,7 +81,7 @@ GRANT execute ON DBMS_LOCK TO <schema-name>;
|
|
81
81
|
|
82
82
|
Workhorse can handle any jobs that support the `perform` method and are
|
83
83
|
serializable. To queue a basic job, use the static method `Workhorse.enqueue`.
|
84
|
-
You can optionally pass a queue name and a
|
84
|
+
You can optionally pass a queue name, a priority and a description (as a string).
|
85
85
|
|
86
86
|
```ruby
|
87
87
|
class MyJob
|
@@ -94,7 +94,7 @@ class MyJob
|
|
94
94
|
end
|
95
95
|
end
|
96
96
|
|
97
|
-
Workhorse.enqueue MyJob.new('John'), queue: :test, priority: 2
|
97
|
+
Workhorse.enqueue MyJob.new('John'), queue: :test, priority: 2, description: 'Basic Job'
|
98
98
|
```
|
99
99
|
|
100
100
|
### RailsOps operations
|
@@ -290,6 +290,10 @@ Workhorse.setup do |config|
|
|
290
290
|
end
|
291
291
|
```
|
292
292
|
|
293
|
+
Using the settings `config.silence_poller_exceptions` and
|
294
|
+
`config.silence_watcher`, you can silence certain exceptions / error outputs
|
295
|
+
(both are disabled by default).
|
296
|
+
|
293
297
|
## Handling database jobs
|
294
298
|
|
295
299
|
Jobs stored in the database can be accessed via the ActiveRecord model
|
@@ -377,7 +381,7 @@ configuration or else using `self.queue_adapter` in a job class inheriting from
|
|
377
381
|
Per default, jobs remain in the database, no matter in which state. This can
|
378
382
|
eventually lead to a very large jobs database. You are advised to clean your
|
379
383
|
jobs database on a regular interval. Workhorse provides the job
|
380
|
-
`
|
384
|
+
`Workhorse::Jobs::CleanupSucceededJobs` for this purpose that cleans up all
|
381
385
|
succeeded jobs. You can run this using your scheduler in a specific interval.
|
382
386
|
|
383
387
|
## Caveats
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.1.0
|
data/lib/workhorse.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
-
require 'socket'
|
2
|
-
require 'active_support/all'
|
3
1
|
require 'active_record'
|
2
|
+
require 'active_support/all'
|
4
3
|
require 'concurrent'
|
4
|
+
require 'socket'
|
5
|
+
require 'uri'
|
5
6
|
|
6
7
|
require 'workhorse/enqueuer'
|
7
8
|
require 'workhorse/scoped_env'
|
@@ -30,6 +31,17 @@ module Workhorse
|
|
30
31
|
# ExceptionNotifier.notify_exception(exception)
|
31
32
|
end
|
32
33
|
|
34
|
+
# If set to `true`, the defined `on_exception` will not be called when the
|
35
|
+
# poller encounters an exception and the worker has to be shut down. The
|
36
|
+
# exception will still be logged.
|
37
|
+
mattr_accessor :silence_poller_exceptions
|
38
|
+
self.silence_poller_exceptions = false
|
39
|
+
|
40
|
+
# If set to `true`, the `watch` command won't produce any output. This does
|
41
|
+
# not include warnings such as the "development mode" warning.
|
42
|
+
mattr_accessor :silence_watcher
|
43
|
+
self.silence_watcher = false
|
44
|
+
|
33
45
|
mattr_accessor :perform_jobs_in_tx
|
34
46
|
self.perform_jobs_in_tx = true
|
35
47
|
|
data/lib/workhorse/daemon.rb
CHANGED
@@ -38,21 +38,21 @@ module Workhorse
|
|
38
38
|
@workers << Worker.new(@workers.size + 1, name, &block)
|
39
39
|
end
|
40
40
|
|
41
|
-
def start
|
41
|
+
def start(quiet: false)
|
42
42
|
code = 0
|
43
43
|
|
44
44
|
for_each_worker do |worker|
|
45
45
|
pid_file, pid = read_pid(worker)
|
46
46
|
|
47
47
|
if pid_file && pid
|
48
|
-
warn "Worker ##{worker.id} (#{worker.name}): Already started (PID #{pid})"
|
48
|
+
warn "Worker ##{worker.id} (#{worker.name}): Already started (PID #{pid})" unless quiet
|
49
49
|
code = 1
|
50
50
|
elsif pid_file
|
51
51
|
File.delete pid_file
|
52
|
-
puts "Worker ##{worker.id} (#{worker.name}): Starting (stale pid file)"
|
52
|
+
puts "Worker ##{worker.id} (#{worker.name}): Starting (stale pid file)" unless quiet
|
53
53
|
start_worker worker
|
54
54
|
else
|
55
|
-
warn "Worker ##{worker.id} (#{worker.name}): Starting"
|
55
|
+
warn "Worker ##{worker.id} (#{worker.name}): Starting" unless quiet
|
56
56
|
start_worker worker
|
57
57
|
end
|
58
58
|
end
|
@@ -109,7 +109,7 @@ module Workhorse
|
|
109
109
|
end
|
110
110
|
|
111
111
|
if should_be_running && status(quiet: true) != 0
|
112
|
-
return start
|
112
|
+
return start(quiet: Workhorse.silence_watcher)
|
113
113
|
else
|
114
114
|
return 0
|
115
115
|
end
|
@@ -177,7 +177,9 @@ module Workhorse
|
|
177
177
|
file = pid_file_for(worker)
|
178
178
|
|
179
179
|
if File.exist?(file)
|
180
|
-
|
180
|
+
raw_pid = IO.read(file)
|
181
|
+
return nil, nil if raw_pid.blank?
|
182
|
+
pid = Integer(raw_pid)
|
181
183
|
return file, process?(pid) ? pid : nil
|
182
184
|
else
|
183
185
|
return nil, nil
|
data/lib/workhorse/db_job.rb
CHANGED
data/lib/workhorse/enqueuer.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
module Workhorse
|
2
2
|
module Enqueuer
|
3
3
|
# Enqueue any object that is serializable and has a `perform` method
|
4
|
-
def enqueue(job, queue: nil, priority: 0, perform_at: Time.now)
|
4
|
+
def enqueue(job, queue: nil, priority: 0, perform_at: Time.now, description: nil)
|
5
5
|
return DbJob.create!(
|
6
6
|
queue: queue,
|
7
7
|
priority: priority,
|
8
8
|
perform_at: perform_at,
|
9
|
+
description: description,
|
9
10
|
handler: Marshal.dump(job)
|
10
11
|
)
|
11
12
|
end
|
@@ -22,7 +22,7 @@ module Workhorse::Jobs
|
|
22
22
|
rel = rel.where('locked_at < ?', @locked_to_started_threshold.seconds.ago)
|
23
23
|
ids = rel.pluck(:id)
|
24
24
|
|
25
|
-
|
25
|
+
unless ids.empty?
|
26
26
|
messages << "Detected #{ids.size} jobs that were locked more than "\
|
27
27
|
"#{@locked_to_started_threshold}s ago and might be stale: #{ids.inspect}."
|
28
28
|
end
|
@@ -34,7 +34,7 @@ module Workhorse::Jobs
|
|
34
34
|
rel = rel.where('started_at < ?', @run_time_threshold.seconds.ago)
|
35
35
|
ids = rel.pluck(:id)
|
36
36
|
|
37
|
-
|
37
|
+
unless ids.empty?
|
38
38
|
messages << "Detected #{ids.size} jobs that are running for longer than "\
|
39
39
|
"#{@run_time_threshold}s ago and might be stale: #{ids.inspect}."
|
40
40
|
end
|
data/lib/workhorse/performer.rb
CHANGED
data/lib/workhorse/poller.rb
CHANGED
@@ -3,8 +3,8 @@ module Workhorse
|
|
3
3
|
MIN_LOCK_TIMEOUT = 0.1 # In seconds
|
4
4
|
MAX_LOCK_TIMEOUT = 1.0 # In seconds
|
5
5
|
|
6
|
-
ORACLE_LOCK_MODE
|
7
|
-
ORACLE_LOCK_HANDLE =
|
6
|
+
ORACLE_LOCK_MODE = 6 # X_MODE (exclusive)
|
7
|
+
ORACLE_LOCK_HANDLE = 478_564_848 # Randomly chosen number
|
8
8
|
|
9
9
|
attr_reader :worker
|
10
10
|
attr_reader :table
|
@@ -35,7 +35,7 @@ module Workhorse
|
|
35
35
|
rescue Exception => e
|
36
36
|
worker.log %(Poll encountered exception:\n#{e.message}\n#{e.backtrace.join("\n")})
|
37
37
|
worker.log 'Worker shutting down...'
|
38
|
-
Workhorse.on_exception.call(e)
|
38
|
+
Workhorse.on_exception.call(e) unless Workhorse.silence_poller_exceptions
|
39
39
|
@running = false
|
40
40
|
worker.instance_variable_get(:@pool).shutdown
|
41
41
|
break
|
@@ -72,7 +72,7 @@ module Workhorse
|
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
75
|
-
def with_global_lock(name: :workhorse, timeout: 2, &
|
75
|
+
def with_global_lock(name: :workhorse, timeout: 2, &_block)
|
76
76
|
if @is_oracle
|
77
77
|
result = Workhorse::DbJob.connection.select_all(
|
78
78
|
"SELECT DBMS_LOCK.REQUEST(#{ORACLE_LOCK_HANDLE}, #{ORACLE_LOCK_MODE}, #{timeout}) FROM DUAL"
|
@@ -105,6 +105,8 @@ module Workhorse
|
|
105
105
|
timeout = [MIN_LOCK_TIMEOUT, [MAX_LOCK_TIMEOUT, worker.polling_interval].min].max
|
106
106
|
|
107
107
|
with_global_lock timeout: timeout do
|
108
|
+
job_ids = []
|
109
|
+
|
108
110
|
Workhorse.tx_callback.call do
|
109
111
|
# As we are the only thread posting into the worker pool, it is safe to
|
110
112
|
# get the number of idle threads without mutex synchronization. The
|
@@ -119,10 +121,12 @@ module Workhorse
|
|
119
121
|
jobs.each do |job|
|
120
122
|
worker.log "Marking job #{job.id} as locked", :debug
|
121
123
|
job.mark_locked!(worker.id)
|
122
|
-
|
124
|
+
job_ids << job.id
|
123
125
|
end
|
124
126
|
end
|
125
127
|
end
|
128
|
+
|
129
|
+
job_ids.each { |job_id| worker.perform(job_id) }
|
126
130
|
end
|
127
131
|
end
|
128
132
|
|
data/lib/workhorse/worker.rb
CHANGED
@@ -127,14 +127,14 @@ module Workhorse
|
|
127
127
|
@pool.idle
|
128
128
|
end
|
129
129
|
|
130
|
-
def perform(
|
130
|
+
def perform(db_job_id)
|
131
131
|
mutex.synchronize do
|
132
132
|
assert_state! :running
|
133
|
-
log "Posting job #{
|
133
|
+
log "Posting job #{db_job_id} to thread pool"
|
134
134
|
|
135
135
|
@pool.post do
|
136
136
|
begin
|
137
|
-
Workhorse::Performer.new(
|
137
|
+
Workhorse::Performer.new(db_job_id, self).perform
|
138
138
|
rescue Exception => e
|
139
139
|
log %(#{e.message}\n#{e.backtrace.join("\n")}), :error
|
140
140
|
end
|
data/test/lib/db_schema.rb
CHANGED
@@ -33,6 +33,15 @@ class Workhorse::EnqueuerTest < WorkhorseTest
|
|
33
33
|
assert_equal 1, Workhorse::DbJob.first.priority
|
34
34
|
end
|
35
35
|
|
36
|
+
def test_with_description
|
37
|
+
assert_equal 0, Workhorse::DbJob.all.count
|
38
|
+
Workhorse.enqueue BasicJob.new, description: 'Lorem ipsum'
|
39
|
+
assert_equal 1, Workhorse::DbJob.all.count
|
40
|
+
|
41
|
+
db_job = Workhorse::DbJob.first
|
42
|
+
assert_equal 'Lorem ipsum', db_job.description
|
43
|
+
end
|
44
|
+
|
36
45
|
def test_op
|
37
46
|
Workhorse.enqueue_op DummyRailsOpsOp, { queue: :q1 }, foo: :bar
|
38
47
|
|
@@ -48,7 +48,7 @@ class Workhorse::PollerTest < WorkhorseTest
|
|
48
48
|
assert_equal %w[q1 q2], w.poller.send(:valid_queues)
|
49
49
|
end
|
50
50
|
|
51
|
-
def
|
51
|
+
def test_valid_queues_2
|
52
52
|
w = Workhorse::Worker.new(polling_interval: 60)
|
53
53
|
|
54
54
|
assert_equal [], w.poller.send(:valid_queues)
|
@@ -148,6 +148,7 @@ class Workhorse::PollerTest < WorkhorseTest
|
|
148
148
|
assert_equal 25, used_workers
|
149
149
|
end
|
150
150
|
|
151
|
+
# rubocop: disable Style/GlobalVars
|
151
152
|
def test_connection_loss
|
152
153
|
$thread_conn = nil
|
153
154
|
|
@@ -174,6 +175,7 @@ class Workhorse::PollerTest < WorkhorseTest
|
|
174
175
|
|
175
176
|
assert_equal 1, Workhorse::DbJob.succeeded.count
|
176
177
|
end
|
178
|
+
# rubocop: enable Style/GlobalVars
|
177
179
|
|
178
180
|
private
|
179
181
|
|
data/workhorse.gemspec
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
# stub: workhorse 1.
|
2
|
+
# stub: workhorse 1.1.0 ruby lib
|
3
3
|
|
4
4
|
Gem::Specification.new do |s|
|
5
5
|
s.name = "workhorse".freeze
|
6
|
-
s.version = "1.
|
6
|
+
s.version = "1.1.0"
|
7
7
|
|
8
|
-
s.required_rubygems_version = Gem::Requirement.new("
|
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 = "2020-
|
11
|
+
s.date = "2020-12-24"
|
12
12
|
s.files = [".gitignore".freeze, ".releaser_config".freeze, ".rubocop.yml".freeze, ".travis.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]
|
13
13
|
s.rubygems_version = "3.0.3".freeze
|
14
14
|
s.summary = "Multi-threaded job backend with database queuing for ruby.".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.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sitrox
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-12-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -192,8 +192,8 @@ dependencies:
|
|
192
192
|
- - ">="
|
193
193
|
- !ruby/object:Gem::Version
|
194
194
|
version: '0'
|
195
|
-
description:
|
196
|
-
email:
|
195
|
+
description:
|
196
|
+
email:
|
197
197
|
executables: []
|
198
198
|
extensions: []
|
199
199
|
extra_rdoc_files: []
|
@@ -241,10 +241,10 @@ files:
|
|
241
241
|
- test/workhorse/pool_test.rb
|
242
242
|
- test/workhorse/worker_test.rb
|
243
243
|
- workhorse.gemspec
|
244
|
-
homepage:
|
244
|
+
homepage:
|
245
245
|
licenses: []
|
246
246
|
metadata: {}
|
247
|
-
post_install_message:
|
247
|
+
post_install_message:
|
248
248
|
rdoc_options: []
|
249
249
|
require_paths:
|
250
250
|
- lib
|
@@ -255,12 +255,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
255
255
|
version: '0'
|
256
256
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
257
257
|
requirements:
|
258
|
-
- - "
|
258
|
+
- - ">="
|
259
259
|
- !ruby/object:Gem::Version
|
260
|
-
version:
|
260
|
+
version: '0'
|
261
261
|
requirements: []
|
262
|
-
rubygems_version: 3.1.
|
263
|
-
signing_key:
|
262
|
+
rubygems_version: 3.1.4
|
263
|
+
signing_key:
|
264
264
|
specification_version: 4
|
265
265
|
summary: Multi-threaded job backend with database queuing for ruby.
|
266
266
|
test_files:
|