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 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: