workhorse 0.6.9 → 1.0.1

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: a328b92854a461c8b0e3a67f740b21581c9652b428286fa917c7942208d5ab83
4
- data.tar.gz: 802d62becfdef759b6e1edad34eec6c5c606bf1de5a19ee75347149ccf6118f9
3
+ metadata.gz: 6b381576f799a0b69cf90e0df551cae0f967b1462bb744383933ca3e82be785d
4
+ data.tar.gz: c298d93548baee24c30a739b8ff3ea60ce61e91fcaf4c40cead630b29bb024d9
5
5
  SHA512:
6
- metadata.gz: ac93faa479a9f4b228c75b41d7001eadf60a395986160d86342e7423a7e9712c9abeb7e4e69463eafa7d9b36647868ad123ca9efeb776ece3accf5d28d0ec889
7
- data.tar.gz: c00ef0fc1c007f7a11ff208664c6c68653cd3cdf00f5750178f56484fb95721f0aaec095b0a4e8e091ecab7700bbbddaeeadbd1b9457c1fdd18882aa224737c7
6
+ metadata.gz: cc45be8c2c2ee319867798427b6994d5eba4ad6623419f54c13ed500f2a722b90acc8c5dffbd5d0e92ec6a82b4de9484559eed6798e845159210eaaf00fb324a
7
+ data.tar.gz: 8d7926b6c830b94aad5e1e77f4951ebd68c6d0cfd4baccd4d004d63fa9aa4701673114b97371f72f05806a2616bc5fcf633e747441e860aff13fd4b90bf4d158
@@ -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,61 @@
1
1
  # Workhorse Changelog
2
2
 
3
+ ## 1.0.1 - 2020-12-15
4
+
5
+ * Fix handling of empty pid files
6
+
7
+ ## 1.0.0 - 2020-09-21
8
+
9
+ * Stable release, identical to 1.0.0.beta2 but now extensively battle-tested
10
+
11
+ ## 1.0.0.beta2 - 2020-08-27
12
+
13
+ * Add option `config.silence_poller_exceptions` (default `false`)
14
+
15
+ * Add option `config.silence_watcher` (default `false`)
16
+
17
+ ## 1.0.0.beta1 - 2020-08-20
18
+
19
+ This is a stability release that is still experimental and has to be tested in
20
+ battle before it can be considered stable.
21
+
22
+ * Stop passing ActiveRecord job objects between polling and worker threads to
23
+ avoid AR race conditions. Now only IDs are passed between threads.
24
+
25
+ ## 1.0.0.beta0 - 2020-08-19
26
+
27
+ This is a stability release that is still experimental and has to be tested in
28
+ battle before it can be considered stable.
29
+
30
+ * Simplify locking during polling. Other than locking individual jobs, pollers
31
+ now acquire a global lock. While this can lead to many pollers waiting for
32
+ each others locks, performing a poll is usually done very quickly and the
33
+ performance drawback is to be considered neglegible. This change should work
34
+ around some deadlock issues as well as an issue where a job was obtained by
35
+ more than one poller.
36
+
37
+ * Shut down worker if polling encountered any kind of error (running jobs will
38
+ be completed whenever possible). This leads to potential watcher jobs being
39
+ able to restore the failed process.
40
+
41
+ * Make unit test database connection configurable using environment variables
42
+ `DB_NAME`, `DB_USERNAME`, `DB_PASSWORD` and `DB_HOST`. This is only relevant
43
+ if you are working on workhorse and need to run the unit tests.
44
+
45
+ * Fix misbehaviour where queueless jobs were not picked up by workers as long as
46
+ a named queue was in a locked state.
47
+
48
+ * Add built-in job `Workhorse::Jobs::DetectStaleJobsJob` which you can schedule.
49
+ It picks up jobs that remained `locked` or `started` (running) for more than a
50
+ certain amount of time. If any of these jobs are found, an exception is thrown
51
+ (which may cause a notification if you configured `on_exception` accordingly).
52
+ See the job's API documentation for more information.
53
+
54
+ **If using oracle:** Make sure to grant execute permission to the package
55
+ `DBMS_LOCK` for your oracle database schema:
56
+
57
+ ```GRANT execute ON DBMS_LOCK TO <schema-name>;```
58
+
3
59
  ## 0.6.9 - 2020-04-22
4
60
 
5
61
  * Fix error where processes may have mistakenly been detected as running (add a
data/FAQ.md CHANGED
@@ -74,4 +74,8 @@ production mode.
74
74
 
75
75
  ## Why does workhorse not support timeouts?
76
76
 
77
- Generic timeout implementations are [a dangerous thing](http://www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/) in Ruby. This is why we decided against providing this feature in Workhorse and recommend to implement the timeouts inside of your jobs - i.e. via network timeouts.
77
+ Generic timeout implementations are [a dangerous
78
+ thing](http://www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/)
79
+ in Ruby. This is why we decided against providing this feature in Workhorse and
80
+ recommend to implement the timeouts inside of your jobs - i.e. via network
81
+ timeouts.
data/README.md CHANGED
@@ -66,6 +66,15 @@ What it does not do:
66
66
 
67
67
  Please customize the initializer and worker script to your liking.
68
68
 
69
+ ### Oracle
70
+
71
+ When using Oracle databases, make sure your schema has access to the package
72
+ `DBMS_LOCK`:
73
+
74
+ ```
75
+ GRANT execute ON DBMS_LOCK TO <schema-name>;
76
+ ```
77
+
69
78
  ## Queuing jobs
70
79
 
71
80
  ### Basic jobs
@@ -281,6 +290,10 @@ Workhorse.setup do |config|
281
290
  end
282
291
  ```
283
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
+
284
297
  ## Handling database jobs
285
298
 
286
299
  Jobs stored in the database can be accessed via the ActiveRecord model
@@ -320,7 +333,6 @@ DbJob.started
320
333
  DbJob.succeeded
321
334
  DbJob.failed
322
335
  ```
323
-
324
336
  ### Resetting jobs
325
337
 
326
338
  Jobs in a state other than `waiting` are either being processed or else already
@@ -352,7 +364,6 @@ Performing a reset will reset the job state to `waiting` and it will be
352
364
  processed again. All meta fields will be reset as well. See inline documentation
353
365
  of `Workhorse::DbJob#reset!` for more details.
354
366
 
355
-
356
367
  ## Using workhorse with Rails / ActiveJob
357
368
 
358
369
  While workhorse can be used though its custom interface as documented above, it
@@ -373,6 +384,37 @@ jobs database on a regular interval. Workhorse provides the job
373
384
  `Workhose::Jobs::CleanupSucceededJobs` for this purpose that cleans up all
374
385
  succeeded jobs. You can run this using your scheduler in a specific interval.
375
386
 
387
+ ## Caveats
388
+
389
+ ### Errors during polling / crashed workers
390
+
391
+ Each worker process includes one thread that polls the database for jobs and
392
+ dispatches them to individual worker threads. In case of an error in the poller
393
+ (usually due to a database connection drop), the poller aborts and gracefully
394
+ shuts down the entire worker. Jobs still being processed by this worker are
395
+ attempted to be completed during this shutdown (which only works if the database
396
+ connection is still active).
397
+
398
+ This means that you should always have an external *watcher* (usually a
399
+ cronjob), that calls the `workhorse watch` command regularly. This would
400
+ automatically restart crashed worker processes.
401
+
402
+ ### Stuck queues
403
+
404
+ Jobs in named queues (non-null queues) are always run sequentially. This means
405
+ that if a job in such a queue is stuck in states `locked` or `started` (i.e. due
406
+ to a database connection failure), no more jobs of this queue will be run as the
407
+ entire queue is considered locked to ensure that no jobs of the same queue run
408
+ in parallel.
409
+
410
+ For this purpose, Workhorse provides the built-in job
411
+ `Workhorse::Jobs::DetectStaleJobsJob` which you are advised schedule on a
412
+ regular basis. It picks up jobs that remained `locked` or `started` (running)
413
+ for more than a certain amount of time. If any of these jobs are found, an
414
+ exception is thrown (which may cause a notification if you configured
415
+ `on_exception` accordingly). See the job's API documentation for more
416
+ information.
417
+
376
418
  ## Frequently asked questions
377
419
 
378
420
  Please consult the [FAQ](FAQ.md).
data/Rakefile CHANGED
@@ -19,6 +19,7 @@ task :gemspec do
19
19
  spec.add_development_dependency 'colorize'
20
20
  spec.add_development_dependency 'benchmark-ips'
21
21
  spec.add_development_dependency 'activejob'
22
+ spec.add_development_dependency 'pry'
22
23
  spec.add_dependency 'activesupport'
23
24
  spec.add_dependency 'activerecord'
24
25
  spec.add_dependency 'schemacop', '~> 2.0'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.9
1
+ 1.0.1
@@ -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
 
@@ -46,6 +58,7 @@ require 'workhorse/worker'
46
58
  require 'workhorse/jobs/run_rails_op'
47
59
  require 'workhorse/jobs/run_active_job'
48
60
  require 'workhorse/jobs/cleanup_succeeded_jobs'
61
+ require 'workhorse/jobs/detect_stale_jobs_job'
49
62
 
50
63
  # Daemon functionality is not available on java platforms
51
64
  if RUBY_PLATFORM != 'java'
@@ -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
@@ -69,7 +69,14 @@ module Workhorse
69
69
  fail "Dirty jobs can't be locked."
70
70
  end
71
71
 
72
+ # TODO: Remove this debug output
73
+ # if Workhorse::DbJob.lock.find(id).locked_at
74
+ # puts "Already locked (with FOR UPDATE)"
75
+ # end
76
+
72
77
  if locked_at
78
+ # TODO: Remove this debug output
79
+ # puts "Already locked. Job: #{self.id} Worker: #{worker_id}"
73
80
  fail "Job #{id} is already locked by #{locked_by.inspect}."
74
81
  end
75
82
 
@@ -1,6 +1,6 @@
1
1
  module Workhorse::Jobs
2
2
  class CleanupSucceededJobs
3
- # Instantiates a new cleanup job
3
+ # Instantiates a new job.
4
4
  #
5
5
  # @param max_age [Integer] The maximal age of jobs to retain, in days. Will
6
6
  # be evaluated at perform time.
@@ -0,0 +1,48 @@
1
+ module Workhorse::Jobs
2
+ class DetectStaleJobsJob
3
+ # Instantiates a new stale detection job.
4
+ #
5
+ # @param locked_to_started_threshold [Integer] The maximum number of seconds
6
+ # a job is allowed to stay 'locked' before this job throws an exception.
7
+ # Set this to 0 to skip this check.
8
+ # @param run_time_threshold [Integer] The maximum number of seconds
9
+ # a job is allowed to run before this job throws an exception. Set this to
10
+ # 0 to skip this check.
11
+ def initialize(locked_to_started_threshold: 3 * 60, run_time_threshold: 12 * 60)
12
+ @locked_to_started_threshold = locked_to_started_threshold
13
+ @run_time_threshold = run_time_threshold
14
+ end
15
+
16
+ def perform
17
+ messages = []
18
+
19
+ # Detect jobs that are locked for too long #
20
+ if @locked_to_started_threshold != 0
21
+ rel = Workhorse::DbJob.locked
22
+ rel = rel.where('locked_at < ?', @locked_to_started_threshold.seconds.ago)
23
+ ids = rel.pluck(:id)
24
+
25
+ unless ids.empty?
26
+ messages << "Detected #{ids.size} jobs that were locked more than "\
27
+ "#{@locked_to_started_threshold}s ago and might be stale: #{ids.inspect}."
28
+ end
29
+ end
30
+
31
+ # Detect jobs that are running for too long #
32
+ if @run_time_threshold != 0
33
+ rel = Workhorse::DbJob.started
34
+ rel = rel.where('started_at < ?', @run_time_threshold.seconds.ago)
35
+ ids = rel.pluck(:id)
36
+
37
+ unless ids.empty?
38
+ messages << "Detected #{ids.size} jobs that are running for longer than "\
39
+ "#{@run_time_threshold}s ago and might be stale: #{ids.inspect}."
40
+ end
41
+ end
42
+
43
+ if messages.any?
44
+ fail messages.join(' ')
45
+ end
46
+ end
47
+ end
48
+ 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
@@ -1,5 +1,11 @@
1
1
  module Workhorse
2
2
  class Poller
3
+ MIN_LOCK_TIMEOUT = 0.1 # In seconds
4
+ MAX_LOCK_TIMEOUT = 1.0 # In seconds
5
+
6
+ ORACLE_LOCK_MODE = 6 # X_MODE (exclusive)
7
+ ORACLE_LOCK_HANDLE = 478_564_848 # Randomly chosen number
8
+
3
9
  attr_reader :worker
4
10
  attr_reader :table
5
11
 
@@ -20,15 +26,20 @@ module Workhorse
20
26
  @running = true
21
27
 
22
28
  @thread = Thread.new do
23
- begin
24
- loop do
25
- break unless running?
29
+ loop do
30
+ break unless running?
31
+
32
+ begin
26
33
  poll
27
34
  sleep
35
+ rescue Exception => e
36
+ worker.log %(Poll encountered exception:\n#{e.message}\n#{e.backtrace.join("\n")})
37
+ worker.log 'Worker shutting down...'
38
+ Workhorse.on_exception.call(e) unless Workhorse.silence_poller_exceptions
39
+ @running = false
40
+ worker.instance_variable_get(:@pool).shutdown
41
+ break
28
42
  end
29
- rescue Exception => e
30
- worker.log %(Poller stopped with exception:\n#{e.message}\n#{e.backtrace.join("\n")})
31
- Workhorse.on_exception.call(e)
32
43
  end
33
44
  end
34
45
  end
@@ -61,41 +72,66 @@ module Workhorse
61
72
  end
62
73
  end
63
74
 
75
+ def with_global_lock(name: :workhorse, timeout: 2, &_block)
76
+ if @is_oracle
77
+ result = Workhorse::DbJob.connection.select_all(
78
+ "SELECT DBMS_LOCK.REQUEST(#{ORACLE_LOCK_HANDLE}, #{ORACLE_LOCK_MODE}, #{timeout}) FROM DUAL"
79
+ ).first.values.last
80
+
81
+ success = result == 0
82
+ else
83
+ result = Workhorse::DbJob.connection.select_all(
84
+ "SELECT GET_LOCK(CONCAT(DATABASE(), '_#{name}'), #{timeout})"
85
+ ).first.values.last
86
+ success = result == 1
87
+ end
88
+
89
+ return unless success
90
+
91
+ yield
92
+ ensure
93
+ if success
94
+ if @is_oracle
95
+ Workhorse::DbJob.connection.execute("SELECT DBMS_LOCK.RELEASE(#{ORACLE_LOCK_HANDLE}) FROM DUAL")
96
+ else
97
+ Workhorse::DbJob.connection.execute("SELECT RELEASE_LOCK(CONCAT(DATABASE(), '_#{name}'))")
98
+ end
99
+ end
100
+ end
101
+
64
102
  def poll
65
103
  @instant_repoll.make_false
66
104
 
67
- Workhorse.tx_callback.call do
68
- # As we are the only thread posting into the worker pool, it is safe to
69
- # get the number of idle threads without mutex synchronization. The
70
- # actual number of idle workers at time of posting can only be larger
71
- # than or equal to the number we get here.
72
- idle = worker.idle
73
-
74
- worker.log "Polling DB for jobs (#{idle} available threads)...", :debug
75
-
76
- unless idle.zero?
77
- jobs = queued_db_jobs(idle)
78
- jobs.each do |job|
79
- worker.log "Marking job #{job.id} as locked", :debug
80
- job.mark_locked!(worker.id)
81
- worker.perform job
105
+ timeout = [MIN_LOCK_TIMEOUT, [MAX_LOCK_TIMEOUT, worker.polling_interval].min].max
106
+
107
+ with_global_lock timeout: timeout do
108
+ job_ids = []
109
+
110
+ Workhorse.tx_callback.call do
111
+ # As we are the only thread posting into the worker pool, it is safe to
112
+ # get the number of idle threads without mutex synchronization. The
113
+ # actual number of idle workers at time of posting can only be larger
114
+ # than or equal to the number we get here.
115
+ idle = worker.idle
116
+
117
+ worker.log "Polling DB for jobs (#{idle} available threads)...", :debug
118
+
119
+ unless idle.zero?
120
+ jobs = queued_db_jobs(idle)
121
+ jobs.each do |job|
122
+ worker.log "Marking job #{job.id} as locked", :debug
123
+ job.mark_locked!(worker.id)
124
+ job_ids << job.id
125
+ end
82
126
  end
83
127
  end
128
+
129
+ job_ids.each { |job_id| worker.perform(job_id) }
84
130
  end
85
131
  end
86
132
 
87
133
  # Returns an Array of #{Workhorse::DbJob}s that can be started
88
134
  def queued_db_jobs(limit)
89
- # ---------------------------------------------------------------
90
- # Lock all queued jobs that are waiting
91
- # ---------------------------------------------------------------
92
- Workhorse::DbJob.connection.execute(
93
- Workhorse::DbJob.select('null').where(
94
- table[:queue].not_eq(nil)
95
- .and(table[:state].eq(:waiting))
96
- ).lock.to_sql
97
- )
98
-
99
135
  # ---------------------------------------------------------------
100
136
  # Select jobs to execute
101
137
  # ---------------------------------------------------------------
@@ -147,20 +183,6 @@ module Workhorse
147
183
  # Limit number of records
148
184
  select = agnostic_limit(select, limit)
149
185
 
150
- # Wrap the entire query in an other subselect to enable locking under
151
- # Oracle SQL. As MySQL is able to lock the records without this additional
152
- # complication, only do this when using the Oracle backend.
153
- if @is_oracle
154
- if AREL_GTE_7
155
- select = Arel::SelectManager.new(Arel.sql('(' + select.to_sql + ')'))
156
- else
157
- select = Arel::SelectManager.new(ActiveRecord::Base, Arel.sql('(' + select.to_sql + ')'))
158
- end
159
- select = table.project(Arel.star).where(table[:id].in(select.project(:id)))
160
- end
161
-
162
- select = select.lock
163
-
164
186
  return Workhorse::DbJob.find_by_sql(select.to_sql).to_a
165
187
  end
166
188
 
@@ -214,7 +236,7 @@ module Workhorse
214
236
  .where(table[:state].in(bad_states))
215
237
  # .distinct is not chainable in older Arel versions
216
238
  bad_queues_select.distinct
217
- select = select.where(table[:queue].not_in(bad_queues_select))
239
+ select = select.where(table[:queue].not_in(bad_queues_select).or(table[:queue].eq(nil)))
218
240
 
219
241
  # Restrict queues to valid ones as indicated by the options given to the
220
242
  # worker
@@ -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
@@ -1,6 +1,8 @@
1
1
  require 'minitest/autorun'
2
2
  require 'active_record'
3
3
  require 'active_job'
4
+ require 'pry'
5
+ require 'colorize'
4
6
  require 'mysql2'
5
7
  require 'benchmark'
6
8
  require 'jobs'
@@ -40,7 +42,14 @@ class WorkhorseTest < ActiveSupport::TestCase
40
42
  end
41
43
  end
42
44
 
43
- ActiveRecord::Base.establish_connection adapter: 'mysql2', database: 'workhorse', username: 'travis', password: '', pool: 10, host: :localhost
45
+ ActiveRecord::Base.establish_connection(
46
+ adapter: 'mysql2',
47
+ database: ENV['DB_NAME'] || 'workhorse',
48
+ username: ENV['DB_USERNAME'] || 'root',
49
+ password: ENV['DB_PASSWORD'] || '',
50
+ host: ENV['DB_HOST'] || '127.0.0.1',
51
+ pool: 10
52
+ )
44
53
 
45
54
  require 'db_schema'
46
55
  require 'workhorse'
@@ -48,6 +48,24 @@ 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_2
52
+ w = Workhorse::Worker.new(polling_interval: 60)
53
+
54
+ assert_equal [], w.poller.send(:valid_queues)
55
+
56
+ Workhorse.enqueue BasicJob.new(sleep_time: 2), queue: nil
57
+
58
+ assert_equal [nil], w.poller.send(:valid_queues)
59
+
60
+ a_job = Workhorse.enqueue BasicJob.new(sleep_time: 2), queue: :a
61
+
62
+ assert_equal [nil, 'a'], w.poller.send(:valid_queues)
63
+
64
+ a_job.update_attribute :state, :locked
65
+
66
+ assert_equal [nil], w.poller.send(:valid_queues)
67
+ end
68
+
51
69
  def test_no_queues
52
70
  w = Workhorse::Worker.new(polling_interval: 60)
53
71
  assert_equal [], w.poller.send(:valid_queues)
@@ -96,6 +114,69 @@ class Workhorse::PollerTest < WorkhorseTest
96
114
  assert_equal 1, Workhorse::DbJob.where(state: :succeeded).count
97
115
  end
98
116
 
117
+ def test_already_locked_issue
118
+ # Create 100 jobs
119
+ 100.times do |i|
120
+ Workhorse.enqueue BasicJob.new(some_param: i, sleep_time: 0)
121
+ end
122
+
123
+ # Create 25 worker processes that work for 10s each
124
+ 25.times do
125
+ Process.fork do
126
+ work 10, pool_size: 1, polling_interval: 0.1
127
+ end
128
+ end
129
+
130
+ # Create additional 100 jobs that are scheduled while the workers are
131
+ # already polling (to make sure those are picked up as well)
132
+ 100.times do
133
+ sleep 0.05
134
+ Workhorse.enqueue BasicJob.new(sleep_time: 0)
135
+ end
136
+
137
+ # Wait for all forked processes to finish (should take ~10s)
138
+ Process.waitall
139
+
140
+ total = Workhorse::DbJob.count
141
+ succeeded = Workhorse::DbJob.succeeded.count
142
+ used_workers = Workhorse::DbJob.lock.pluck(:locked_by).uniq.size
143
+
144
+ # Make sure there are 200 jobs, all jobs have succeeded and that all of the
145
+ # workers have had their turn.
146
+ assert_equal 200, total
147
+ assert_equal 200, succeeded
148
+ assert_equal 25, used_workers
149
+ end
150
+
151
+ # rubocop: disable Style/GlobalVars
152
+ def test_connection_loss
153
+ $thread_conn = nil
154
+
155
+ Workhorse.enqueue BasicJob.new(sleep_time: 3)
156
+
157
+ t = Thread.new do
158
+ w = Workhorse::Worker.new(pool_size: 5, polling_interval: 0.1)
159
+ w.start
160
+
161
+ sleep 0.5
162
+
163
+ w.poller.define_singleton_method :poll do
164
+ fail ActiveRecord::StatementInvalid, 'Mysql2::Error: Connection was killed'
165
+ end
166
+
167
+ w.wait
168
+ end
169
+
170
+ assert_nothing_raised do
171
+ Timeout.timeout(6) do
172
+ t.join
173
+ end
174
+ end
175
+
176
+ assert_equal 1, Workhorse::DbJob.succeeded.count
177
+ end
178
+ # rubocop: enable Style/GlobalVars
179
+
99
180
  private
100
181
 
101
182
  def setup
@@ -1,15 +1,15 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: workhorse 0.6.9 ruby lib
2
+ # stub: workhorse 1.0.1 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "workhorse".freeze
6
- s.version = "0.6.9"
6
+ s.version = "1.0.1"
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 = "2020-04-22"
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/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 = "2020-12-15"
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
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]
@@ -26,6 +26,7 @@ Gem::Specification.new do |s|
26
26
  s.add_development_dependency(%q<colorize>.freeze, [">= 0"])
27
27
  s.add_development_dependency(%q<benchmark-ips>.freeze, [">= 0"])
28
28
  s.add_development_dependency(%q<activejob>.freeze, [">= 0"])
29
+ s.add_development_dependency(%q<pry>.freeze, [">= 0"])
29
30
  s.add_runtime_dependency(%q<activesupport>.freeze, [">= 0"])
30
31
  s.add_runtime_dependency(%q<activerecord>.freeze, [">= 0"])
31
32
  s.add_runtime_dependency(%q<schemacop>.freeze, ["~> 2.0"])
@@ -39,6 +40,7 @@ Gem::Specification.new do |s|
39
40
  s.add_dependency(%q<colorize>.freeze, [">= 0"])
40
41
  s.add_dependency(%q<benchmark-ips>.freeze, [">= 0"])
41
42
  s.add_dependency(%q<activejob>.freeze, [">= 0"])
43
+ s.add_dependency(%q<pry>.freeze, [">= 0"])
42
44
  s.add_dependency(%q<activesupport>.freeze, [">= 0"])
43
45
  s.add_dependency(%q<activerecord>.freeze, [">= 0"])
44
46
  s.add_dependency(%q<schemacop>.freeze, ["~> 2.0"])
@@ -53,6 +55,7 @@ Gem::Specification.new do |s|
53
55
  s.add_dependency(%q<colorize>.freeze, [">= 0"])
54
56
  s.add_dependency(%q<benchmark-ips>.freeze, [">= 0"])
55
57
  s.add_dependency(%q<activejob>.freeze, [">= 0"])
58
+ s.add_dependency(%q<pry>.freeze, [">= 0"])
56
59
  s.add_dependency(%q<activesupport>.freeze, [">= 0"])
57
60
  s.add_dependency(%q<activerecord>.freeze, [">= 0"])
58
61
  s.add_dependency(%q<schemacop>.freeze, ["~> 2.0"])
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: 0.6.9
4
+ version: 1.0.1
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-04-22 00:00:00.000000000 Z
11
+ date: 2020-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: activesupport
127
141
  requirement: !ruby/object:Gem::Requirement
@@ -178,8 +192,8 @@ dependencies:
178
192
  - - ">="
179
193
  - !ruby/object:Gem::Version
180
194
  version: '0'
181
- description:
182
- email:
195
+ description:
196
+ email:
183
197
  executables: []
184
198
  extensions: []
185
199
  extra_rdoc_files: []
@@ -208,6 +222,7 @@ files:
208
222
  - lib/workhorse/db_job.rb
209
223
  - lib/workhorse/enqueuer.rb
210
224
  - lib/workhorse/jobs/cleanup_succeeded_jobs.rb
225
+ - lib/workhorse/jobs/detect_stale_jobs_job.rb
211
226
  - lib/workhorse/jobs/run_active_job.rb
212
227
  - lib/workhorse/jobs/run_rails_op.rb
213
228
  - lib/workhorse/performer.rb
@@ -226,10 +241,10 @@ files:
226
241
  - test/workhorse/pool_test.rb
227
242
  - test/workhorse/worker_test.rb
228
243
  - workhorse.gemspec
229
- homepage:
244
+ homepage:
230
245
  licenses: []
231
246
  metadata: {}
232
- post_install_message:
247
+ post_install_message:
233
248
  rdoc_options: []
234
249
  require_paths:
235
250
  - lib
@@ -244,8 +259,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
244
259
  - !ruby/object:Gem::Version
245
260
  version: '0'
246
261
  requirements: []
247
- rubygems_version: 3.1.2
248
- signing_key:
262
+ rubygems_version: 3.1.4
263
+ signing_key:
249
264
  specification_version: 4
250
265
  summary: Multi-threaded job backend with database queuing for ruby.
251
266
  test_files: