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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5dd13b11d5f7488e58812f57ad2bb70da07b95f18b89653687bf9ec579bbcaa5
4
- data.tar.gz: a0e7c53cf4de56b6109830decf16f6f70a8aead13ca703a795ce7709234c1b05
3
+ metadata.gz: d0994338972770f1cb6f44db74a01c54c7ebdb9aa88afd1e9938a1565997edc8
4
+ data.tar.gz: f48f3df959cca6b69a84f77a501989b6430cc04582e56345a7441af4258f62c0
5
5
  SHA512:
6
- metadata.gz: 2c6e9162d5b2735cd1de678b56e4c4e4960e25776a64cc45dfe5fbfee4fe0e50b1b3a5d15e81ca65ebfe496cce6ac4d81cfe1ba47a65b591a54916807bbe5c68
7
- data.tar.gz: a24094b51e7751010920e39ad6ac3924dbccdfb84f1728efec73ed1cc0e9376a873cdb2ff4a562f49d78b17ae08e5659fd596684b299fc35b8bdcd228be15f0e
6
+ metadata.gz: 2631da0ac85ffb054b9705cee07174a56c56d4b773fcd25b341ece72a6e92244eb10fb88f7748dfd905fb5d01f8fd17b64425b1d479b71b40ca850af607d6581
7
+ data.tar.gz: a4ac3e1f039fc3884e2ef0a066ff2d152b351d87926139a3098fba81ab7bedb8dd80c783e57ea0de79a6187296feee1aea73949207d0f8631af9b4415ab8cb68
@@ -1,7 +1,7 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.3.0
4
3
  - 2.5.0
4
+ - 2.6.0
5
5
  services:
6
6
  - mysql
7
7
  before_install:
@@ -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 priority.
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
- `Workhose::Jobs::CleanupSucceededJobs` for this purpose that cleans up all
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.0.0.beta0
1
+ 1.1.0
@@ -17,6 +17,8 @@ class CreateTableJobs < ActiveRecord::Migration
17
17
  t.integer :priority, null: false
18
18
  t.datetime :perform_at, null: true
19
19
 
20
+ t.string :description, null: true
21
+
20
22
  t.timestamps null: false
21
23
  end
22
24
 
@@ -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
 
@@ -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
- pid = IO.read(file).to_i
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
@@ -7,7 +7,7 @@ module Workhorse
7
7
  STATE_FAILED = :failed
8
8
 
9
9
  if respond_to?(:attr_accessible)
10
- attr_accessible :queue, :priority, :perform_at, :handler
10
+ attr_accessible :queue, :priority, :perform_at, :handler, :description
11
11
  end
12
12
 
13
13
  self.table_name = 'jobs'
@@ -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
- if ids.size > 0
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
- if ids.size > 0
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
@@ -2,8 +2,8 @@ module Workhorse
2
2
  class Performer
3
3
  attr_reader :worker
4
4
 
5
- def initialize(db_job, worker)
6
- @db_job = db_job
5
+ def initialize(db_job_id, worker)
6
+ @db_job = Workhorse::DbJob.find(db_job_id)
7
7
  @worker = worker
8
8
  @started = false
9
9
  end
@@ -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 = 6 # X_MODE (exclusive)
7
- ORACLE_LOCK_HANDLE = 478564848 # Randomly chosen number
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, &block)
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
- worker.perform job
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
 
@@ -127,14 +127,14 @@ module Workhorse
127
127
  @pool.idle
128
128
  end
129
129
 
130
- def perform(db_job)
130
+ def perform(db_job_id)
131
131
  mutex.synchronize do
132
132
  assert_state! :running
133
- log "Posting job #{db_job.id} to thread pool"
133
+ log "Posting job #{db_job_id} to thread pool"
134
134
 
135
135
  @pool.post do
136
136
  begin
137
- Workhorse::Performer.new(db_job, self).perform
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
@@ -18,6 +18,8 @@ ActiveRecord::Schema.define do
18
18
  t.integer :priority, null: false
19
19
  t.datetime :perform_at, null: true
20
20
 
21
+ t.string :description, null: true
22
+
21
23
  t.timestamps null: false
22
24
  end
23
25
 
@@ -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 test_valid_queues
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
 
@@ -1,14 +1,14 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: workhorse 1.0.0.beta0 ruby lib
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.0.0.beta0"
6
+ s.version = "1.1.0"
7
7
 
8
- s.required_rubygems_version = Gem::Requirement.new("> 1.3.1".freeze) if s.respond_to? :required_rubygems_version=
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-08-19"
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.0.0.beta0
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-08-19 00:00:00.000000000 Z
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: 1.3.1
260
+ version: '0'
261
261
  requirements: []
262
- rubygems_version: 3.1.2
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: