workhorse 1.0.0.beta0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|