workhorse 0.2.0 → 0.3.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
  SHA1:
3
- metadata.gz: 0cf16c2d211462d7875c56595c5d624ff30b8cfe
4
- data.tar.gz: 4772dce847b5b7aa51f1931baa7a878d34bc0e06
3
+ metadata.gz: da01b73fc0a4be6598393d2c4b0fbbd917c83233
4
+ data.tar.gz: 637033b135c6f8fb1c4ad178c05954ba3d495a22
5
5
  SHA512:
6
- metadata.gz: ca5aec71e437a2e151fa0b3356185c44bc12cb60cf09d817097e15307320247083eab40553da836e3dff911cfb98747c1a991755a72d30bb84f7bbd714544a55
7
- data.tar.gz: 1dcfe3bda9f7c1a6d7dac78bf6a795cadb0a59248ec1da72e3a0fba89b91745b7053938f0f2a4570bf48155ae233968399b01a48e6620a53927f59b54444add2
6
+ metadata.gz: cb5a848ac259293e090c931d3faed8baa23a96087a5724802855340f60a85256e0da57ca30c4b2b600694330fa35d760f2320cd41a6ed3f7a2c88b253f50c8ed
7
+ data.tar.gz: 8d20351d7fcd8bde6f05eb186efd3b9f64e488d1e3da91b62b489e2564b6df7a2a23983cdb74243c1fc086ec97d504088a2ee5b430b661feb8c2c08065205651
data/CHANGELOG.md CHANGED
@@ -1,9 +1,27 @@
1
1
  # Workhorse Change log
2
2
 
3
- ## 0.2.0 - 2017-12-19
3
+ ## 0.3.0 2017-12-27
4
+
5
+ ### Added
6
+
7
+ * Option `perform_at` to set earliest execution time of a job
8
+ * Stock job for clean-up of succeeded jobs
9
+ * Example for safe scheduling of repeating jobs
10
+
11
+ ### Changed
12
+
13
+ * Improved help text output for the daemon
14
+ * Polling intervals can now be multiples of 0.1 instead of integers
15
+
16
+ ### Fixed
17
+
18
+ * Respect queues even when no job of that queue is running
19
+ * Initial migration now works for both Oracle and MySQL
20
+
21
+ ## 0.2.0 – 2017-12-19
4
22
 
5
23
  * Adds support for job-level priorities
6
24
 
7
- ## 0.1.0 - 2017-12-18
25
+ ## 0.1.0 2017-12-18
8
26
 
9
27
  * First feature-complete production release
data/FAQ.md CHANGED
@@ -66,6 +66,12 @@ Make sure to always start the worker in *production mode*, i.e.:
66
66
  RAILS_ENV=production bin/workhorse.rb start
67
67
  ```
68
68
 
69
+ ## I'm getting "No live threads left. Deadlock?" exceptions
70
+
71
+ Make sure the Worker is logging somewhere and check the logs. Typically there is
72
+ an underlying error that leads to the exception, e.g. missing migration in
73
+ production mode.
74
+
69
75
  ## Why does workhorse not support timeouts?
70
76
 
71
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.
data/README.md CHANGED
@@ -11,21 +11,23 @@ How it works:
11
11
 
12
12
  * Jobs are instances of classes that support the `perform` method.
13
13
  * Jobs are persisted in the database using ActiveRecord.
14
+ * Each job has a priority, the default being 0. Jobs with higher priorities
15
+ (lower is higher, 0 the highest) get processed first.
16
+ * Each job can be set to execute after a certain date / time.
14
17
  * You can start one or more worker processes.
15
18
  * Each worker is configurable as to which queue(s) it processes. Jobs in the
16
19
  same queue never run simultaneously. Jobs with no queue can always run in
17
20
  parallel.
18
- * Each job has a priority, the default being 0. Jobs with higher priorities
19
- (lower is higher, 0 the highest) get processed first.
20
- * Each worker polls the database and spawns a number of threads to execute jobs
21
- of different queues simultaneously.
21
+ * Each worker polls the database and spawns a configurable number of threads to
22
+ execute jobs of different queues simultaneously.
22
23
 
23
24
  What it does not do:
24
25
 
25
26
  * It does not spawn new processes on the fly. Jobs are run in separate threads
26
27
  but not in separate processes (unless you manually start multiple worker
27
28
  processes).
28
- * It does not support retries, timeouts, and timed execution.
29
+ * It does not support
30
+ [timeouts](FAQ.md#why-does-workhorse-not-support-timeouts) and timed execution.
29
31
 
30
32
  ## Installation
31
33
 
@@ -35,8 +37,8 @@ What it does not do:
35
37
  MySQL with InnoDB, PostgreSQL, or Oracle).
36
38
  * If you are planning on using the daemons handler:
37
39
  * An operating system and file system that supports file locking.
38
- * MRI ruby (aka "c ruby") as jRuby does not support `fork`. See the
39
- [FAQ](FAQ.md##im-using-jruby-how-can-i-use-the-daemon-handler) for possible workarounds.
40
+ * MRI ruby (aka "CRuby") as jRuby does not support `fork`. See the
41
+ [FAQ](FAQ.md#im-using-jruby-how-can-i-use-the-daemon-handler) for possible workarounds.
40
42
 
41
43
  ### Installing under Rails
42
44
 
@@ -51,7 +53,7 @@ What it does not do:
51
53
  2. Run the install generator:
52
54
 
53
55
  ```bash
54
- bin/rails generate workhorse:install
56
+ bundle exec rails generate workhorse:install
55
57
  ```
56
58
 
57
59
  This generates:
@@ -100,10 +102,87 @@ at job execution.
100
102
  If you do not want to pass any params to the operation, just omit the second hash:
101
103
 
102
104
  ```ruby
103
- Workhorse.enqueue_op Operations::Jobs::CleanUpDatabase, queue: :maintenance,
104
- priority: 2
105
+ Workhorse.enqueue_op Operations::Jobs::CleanUpDatabase, queue: :maintenance, priority: 2
105
106
  ```
106
107
 
108
+ ### Scheduling
109
+
110
+ Workhorse has no out-of-the-box functionality to support scheduling of regular
111
+ jobs, such as maintenance or backup jobs. There are two primary ways of
112
+ achieving regular execution:
113
+
114
+ 1. Rescheduling by the same job after successful execution and setting
115
+ `perform_at`
116
+
117
+ This is simple to set up and requires no additional dependencies. However,
118
+ the time taken to execute a job and the time delay caused by the polling
119
+ interval cannot easily be factored into the calculation of the interval,
120
+ leading to a slight shift in effective execution date. (This can be mitigated
121
+ by scheduling the job before knowing whether the current run will succeed.
122
+ Proceed down this path at your own peril!)
123
+
124
+ *Example:* A job that takes 5 seconds to run and is set to reschedule itself
125
+ after 10 minutes is started at 12:00 sharp. After one hour it will be set to
126
+ execute at 13:00:30 at the earliest.
127
+
128
+ In its most basic form, the `perform` method of a job would look as follows:
129
+
130
+ ```ruby
131
+ class MyJob
132
+ def perform
133
+ # Do all the work
134
+
135
+ # Perform again after 10 minutes (600 seconds)
136
+ Workhorse.enqueue MyJob.new, perform_at: Time.now + 600
137
+ end
138
+ end
139
+ ```
140
+
141
+ 2. Using an external scheduler
142
+
143
+ A more elaborate setup requires an external scheduler, but which can still be
144
+ called from Ruby. One such scheduler is
145
+ [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler). A small
146
+ example of an adapted `bin/workhorse.rb` to accommodate for the additional
147
+ cog in the mechanism is given below:
148
+
149
+ ```ruby
150
+ #!/usr/bin/env ruby
151
+
152
+ require './config/environment'
153
+
154
+ Workhorse::Daemon::ShellHandler.run do
155
+ worker = Workhorse::Worker.new(pool_size: 5, polling_interval: 10, logger: Rails.logger)
156
+ scheduler = Rufus::Scheduler.new
157
+
158
+ worker.start
159
+
160
+ scheduler.cron '0/10 * * * *' do
161
+ Workhorse.enqueue Workhorse::Jobs::CleanupSucceededJobs.new
162
+ end
163
+
164
+ Signal.trap 'TERM' do
165
+ scheduler.shutdown
166
+ Thread.new do
167
+ worker.shutdown
168
+ end.join
169
+ end
170
+
171
+ scheduler.join
172
+ worker.wait
173
+ end
174
+ ```
175
+
176
+ This allows starting and stopping the daemon with the usual interface.
177
+ Note that the scheduler is handled like a Workhorse worker, the consequence
178
+ of which is that only one 'worker' should be started by the ShellHandler.
179
+ Otherwise there would be multiple jobs scheduled at the same time.
180
+
181
+ Please refer to the documentation on
182
+ [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler) (or the
183
+ scheduler of your choice) for further options concerning the timing of the
184
+ jobs.
185
+
107
186
  ## Configuring and starting workers
108
187
 
109
188
  Workers poll the database for new jobs and execute them in one or more threads.
@@ -143,7 +222,7 @@ For this case, the workhorse install routine automatically creates the file
143
222
  The script can be called as follows:
144
223
 
145
224
  ```bash
146
- RAILS_ENV=production bin/workhorse.rb start|stop|status|watch|restart|usage
225
+ RAILS_ENV=production bundle exec bin/workhorse.rb start|stop|status|watch|restart|usage
147
226
  ```
148
227
 
149
228
  #### Background and customization
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.3.0
@@ -15,11 +15,24 @@ class CreateTableJobs < ActiveRecord::Migration[4.2]
15
15
  t.text :last_error, limit: 4_294_967_295
16
16
 
17
17
  t.integer :priority, null: false
18
+ t.datetime :perform_at, null: true
18
19
 
19
20
  t.timestamps null: false
20
21
  end
21
22
 
22
- add_index :jobs, :queue
23
- add_index :jobs, :state
23
+ if oracle?
24
+ add_index :jobs, :queue
25
+ add_index :jobs, :state
26
+ else
27
+ add_index :jobs, :queue, length: 191
28
+ add_index :jobs, :state, length: 191
29
+ end
30
+ add_index :jobs, :perform_at
31
+ end
32
+
33
+ private
34
+
35
+ def oracle?
36
+ ActiveRecord::Base.connection.adapter_name == 'OracleEnhanced'
24
37
  end
25
38
  end
data/lib/workhorse.rb CHANGED
@@ -37,6 +37,7 @@ require 'workhorse/poller'
37
37
  require 'workhorse/pool'
38
38
  require 'workhorse/worker'
39
39
  require 'workhorse/jobs/run_rails_op'
40
+ require 'workhorse/jobs/cleanup_succeeded_jobs'
40
41
 
41
42
  # Daemon functionality is not available on java platforms
42
43
  if RUBY_PLATFORM != 'java'
@@ -44,6 +44,28 @@ module Workhorse
44
44
  warn <<~USAGE
45
45
  Usage: #{$PROGRAM_NAME} start|stop|status|watch|restart|usage
46
46
 
47
+ Options:
48
+
49
+ start
50
+ Start the daemon
51
+
52
+ stop
53
+ Stop the daemon
54
+
55
+ status
56
+ Query the status of the daemon. Exit with status 1 if any worker is
57
+ not running.
58
+
59
+ watch
60
+ Checks the status (running or stopped) and whether it is as
61
+ expected. Starts the daemon if it is expected to run but is not.
62
+
63
+ restart
64
+ Shortcut for consecutive 'stop' and 'start'.
65
+
66
+ usage
67
+ Show this message
68
+
47
69
  Exit status:
48
70
  0 if OK,
49
71
  1 if at least one worker has an unexpected status,
@@ -1,10 +1,11 @@
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)
4
+ def enqueue(job, queue: nil, priority: 0, perform_at: Time.now)
5
5
  return DbJob.create!(
6
6
  queue: queue,
7
7
  priority: priority,
8
+ perform_at: perform_at,
8
9
  handler: Marshal.dump(job)
9
10
  )
10
11
  end
@@ -0,0 +1,24 @@
1
+ module Workhorse::Jobs
2
+ class CleanupSucceededJobs
3
+ # Instantiates a new cleanup job
4
+ #
5
+ # @param max_age [Integer] The maximal age of jobs to retain, in days. Will
6
+ # be evaluated at perform time.
7
+ def initialize(max_age: 14)
8
+ @max_age = max_age
9
+ end
10
+
11
+ def perform
12
+ age_limit = seconds_ago(@max_age)
13
+ Workhorse::DbJob.where(
14
+ 'STATE = ? AND UPDATED_AT <= ?', Workhorse::DbJob::STATE_SUCCEEDED, age_limit
15
+ ).delete_all
16
+ end
17
+
18
+ private
19
+
20
+ def seconds_ago(days)
21
+ Time.now - days * 24 * 60 * 60
22
+ end
23
+ end
24
+ end
@@ -1,10 +1,13 @@
1
1
  module Workhorse
2
2
  class Poller
3
3
  attr_reader :worker
4
+ attr_reader :table
4
5
 
5
6
  def initialize(worker)
6
7
  @worker = worker
7
8
  @running = false
9
+ @table = Workhorse::DbJob.arel_table
10
+ @is_oracle = ActiveRecord::Base.connection.adapter_name == 'OracleEnhanced'
8
11
  end
9
12
 
10
13
  def running?
@@ -44,8 +47,8 @@ module Workhorse
44
47
  remaining = worker.polling_interval
45
48
 
46
49
  while running? && remaining > 0
47
- Kernel.sleep 1
48
- remaining -= 1
50
+ Kernel.sleep 0.1
51
+ remaining -= 0.1
49
52
  end
50
53
  end
51
54
 
@@ -70,10 +73,8 @@ module Workhorse
70
73
  end
71
74
  end
72
75
 
76
+ # Returns an Array of #{Workhorse::DbJob}s that can be started
73
77
  def queued_db_jobs(limit)
74
- table = Workhorse::DbJob.arel_table
75
- is_oracle = ActiveRecord::Base.connection.adapter_name == 'OracleEnhanced'
76
-
77
78
  # ---------------------------------------------------------------
78
79
  # Lock all queued jobs that are waiting
79
80
  # ---------------------------------------------------------------
@@ -87,9 +88,100 @@ module Workhorse
87
88
  # ---------------------------------------------------------------
88
89
  # Select jobs to execute
89
90
  # ---------------------------------------------------------------
91
+ #
92
+ # Construct selects for each queue which then are UNIONed for the final
93
+ # set. This is required because we only want the first job of each queue
94
+ # to be posted.
95
+ union_parts = []
96
+ valid_queues.each do |queue|
97
+ # Start with a fresh select, as we now know the allowed queues
98
+ select = valid_ordered_select
99
+ select = select.where(table[:queue].eq(queue))
100
+
101
+ # Get the maximum amount possible for no-queue jobs. This gives us the
102
+ # smallest possible set from which to draw the final set of jobs without
103
+ # any presumptions on the order.
104
+ record_number = queue.nil? ? limit : 1
105
+
106
+ union_parts << agnostic_limit(select, record_number)
107
+ end
108
+
109
+ return [] if union_parts.empty?
110
+
111
+ # Combine the jobs of each queue in a giant UNION chain. Arel does not
112
+ # support this directly, as it does not generate parentheses around the
113
+ # subselects.
114
+ # Furthermore, add the alias directly instead of using Arel `as`, because
115
+ # it uses the keyword 'AS' in SQL generated for Oracle, which is invalid
116
+ # for table aliases.
117
+ union_query_sql = '('
118
+ union_query_sql += '(' + union_parts.shift.to_sql + ')'
119
+ union_parts.each do |part|
120
+ union_query_sql += ' UNION (' + part.to_sql + ')'
121
+ end
122
+ union_query_sql += ') subselect'
123
+
124
+ # Create a new SelectManager to work with, using the UNION as data source
125
+ select = Arel::SelectManager.new(Arel.sql(union_query_sql))
126
+ select = order(select.project(Arel.star))
127
+
128
+ # Limit number of records
129
+ select = agnostic_limit(select, limit)
130
+
131
+ # Wrap the entire query in an other subselect to enable locking under
132
+ # Oracle SQL. As MySQL is able to lock the records without this additional
133
+ # complication, only do this when using the Oracle backend.
134
+ if @is_oracle
135
+ select = Arel::SelectManager.new(Arel.sql('(' + select.to_sql + ')'))
136
+ select = table.project(Arel.star).where(table[:id].in(select.project(:id)))
137
+ end
138
+
139
+ select = select.lock
140
+
141
+ return Workhorse::DbJob.find_by_sql(select.to_sql)
142
+ end
90
143
 
91
- # Fetch all waiting jobs of the correct queues
92
- select = table.project(Arel.sql('*')).where(table[:state].eq(:waiting))
144
+ # Returns a fresh Arel select manager containing all waiting jobs, ordered
145
+ # with {#order}.
146
+ #
147
+ # @return [Arel::SelectManager] the select manager
148
+ def valid_ordered_select
149
+ select = table.project(table[Arel.star])
150
+ select = select.where(table[:state].eq(:waiting))
151
+ select = select.where(table[:perform_at].lteq(Time.now).or(table[:perform_at].eq(nil)))
152
+ return order(select)
153
+ end
154
+
155
+ # Orders the records by execution order (first to last)
156
+ #
157
+ # @param select [Arel::SelectManager] the select manager to sort
158
+ # @return [Arel::SelectManager] the passed select manager with sorting on
159
+ # top
160
+ def order(select)
161
+ select.order(Arel.sql('priority').asc).order(Arel.sql('created_at').asc)
162
+ end
163
+
164
+ # Limits the number of records
165
+ #
166
+ # @param select [Arel::SelectManager] the select manager on which to apply
167
+ # the limit
168
+ # @param number [Integer] the maximum number of records to return
169
+ # @return [Arel::SelectManager] the resultant select manager
170
+ def agnostic_limit(select, number)
171
+ return select.where(Arel.sql('ROWNUM').lteq(number)) if @is_oracle
172
+ return select.take(number)
173
+ end
174
+
175
+ # Returns an Array of queue names for which a job may be posted
176
+ #
177
+ # This is done in multiple steps. First, all queues with jobs that are in
178
+ # progress are removed. Second, restrict to only queues for which we may
179
+ # post jobs. Third, extract the queue names of the remaining queues and
180
+ # return them in an Array.
181
+ #
182
+ # @return [Array] an array of unique queue names
183
+ def valid_queues
184
+ select = valid_ordered_select
93
185
 
94
186
  # Restrict queues that are currently in progress
95
187
  bad_queries_select = table.project(table[:queue])
@@ -97,7 +189,8 @@ module Workhorse
97
189
  .distinct
98
190
  select = select.where(table[:queue].not_in(bad_queries_select))
99
191
 
100
- # Restrict queues to "open" ones
192
+ # Restrict queues to valid ones as indicated by the options given to the
193
+ # worker
101
194
  unless worker.queues.empty?
102
195
  if worker.queues.include?(nil)
103
196
  where = table[:queue].eq(nil)
@@ -112,19 +205,11 @@ module Workhorse
112
205
  select = select.where(where)
113
206
  end
114
207
 
115
- # Order by creation date
116
- select = select.order(table[:priority].asc).order(table[:created_at].asc)
117
-
118
- # Limit number of records
119
- if is_oracle
120
- select = select.where(Arel.sql('ROWNUM').lteq(limit))
121
- else
122
- select = select.take(limit)
123
- end
124
-
125
- select = select.lock
126
-
127
- return Workhorse::DbJob.find_by_sql(select.to_sql)
208
+ # Get the names of all valid queues. The extra project here allows
209
+ # selecting the last value in each row of the resulting array and getting
210
+ # the queue name.
211
+ queues = select.project(:queue)
212
+ return Workhorse::DbJob.find_by_sql(queues.to_sql).map(&:queue).uniq
128
213
  end
129
214
  end
130
215
  end
@@ -29,8 +29,7 @@ module Workhorse
29
29
  # number of given queues + 1.
30
30
  # @param polling_interval [Integer] Interval in seconds the database will
31
31
  # be polled for new jobs. Set this as high as possible to avoid
32
- # unnecessary database load Set this as high as possible to avoid
33
- # unnecessary database load.
32
+ # unnecessary database load. Defaults to 5 minutes.
34
33
  # @param auto_terminate [Boolean] Whether to automatically shut down the
35
34
  # worker properly on INT and TERM signals.
36
35
  # @param quiet [Boolean] If this is set to `false`, the worker will also log
@@ -38,7 +37,7 @@ module Workhorse
38
37
  # @param logger [Logger] An optional logger the worker will append to. This
39
38
  # can be any instance of ruby's `Logger` but is commonly set to
40
39
  # `Rails.logger`.
41
- def initialize(queues: [], pool_size: nil, polling_interval: 5, auto_terminate: true, quiet: true, logger: nil)
40
+ def initialize(queues: [], pool_size: nil, polling_interval: 300, auto_terminate: true, quiet: true, logger: nil)
42
41
  @queues = queues
43
42
  @pool_size = pool_size || queues.size + 1
44
43
  @polling_interval = polling_interval
@@ -51,7 +50,9 @@ module Workhorse
51
50
  @poller = Workhorse::Poller.new(self)
52
51
  @logger = logger
53
52
 
54
- fail 'Polling interval must be an integer.' unless @polling_interval.is_a?(Integer)
53
+ unless (@polling_interval / 0.1).round(2).modulo(1) == 0.0
54
+ fail 'Polling interval must be a multiple of 0.1.'
55
+ end
55
56
 
56
57
  check_rails_env if defined?(Rails)
57
58
  end
@@ -16,7 +16,12 @@ ActiveRecord::Schema.define do
16
16
  t.text :last_error, limit: 4_294_967_295
17
17
 
18
18
  t.integer :priority, null: false
19
+ t.datetime :perform_at, null: true
19
20
 
20
21
  t.timestamps null: false
21
22
  end
23
+
24
+ add_index :jobs, :queue, length: 191
25
+ add_index :jobs, :state, length: 191
26
+ add_index :jobs, :perform_at
22
27
  end
@@ -36,9 +36,9 @@ class Workhorse::EnqueuerTest < WorkhorseTest
36
36
  def test_op
37
37
  Workhorse.enqueue_op DummyRailsOpsOp, { queue: :q1 }, foo: :bar
38
38
 
39
- w = Workhorse::Worker.new(queues: [:q1])
39
+ w = Workhorse::Worker.new(queues: [:q1], polling_interval: 0.2)
40
40
  w.start
41
- sleep 1
41
+ sleep 0.2
42
42
  w.shutdown
43
43
 
44
44
  assert_equal 'succeeded', Workhorse::DbJob.first.state
@@ -4,13 +4,11 @@ class Workhorse::WorkerTest < WorkhorseTest
4
4
  # This test makes sure that concurrent jobs always work in different database
5
5
  # connections.
6
6
  def test_db_connections
7
- w = Workhorse::Worker.new polling_interval: 1, pool_size: 5
8
7
  2.times do
9
8
  Workhorse.enqueue DbConnectionTestJob.new
10
9
  end
11
- w.start
12
- sleep 1
13
- w.shutdown
10
+
11
+ work 0.2, polling_interval: 0.2
14
12
 
15
13
  assert_equal 2, DbConnectionTestJob.db_connections.count
16
14
  assert_equal 2, DbConnectionTestJob.db_connections.uniq.count
@@ -4,9 +4,9 @@ class Workhorse::PollerTest < WorkhorseTest
4
4
  def test_interruptable_sleep
5
5
  w = Workhorse::Worker.new(polling_interval: 60)
6
6
  w.start
7
- sleep 0.5
7
+ sleep 0.1
8
8
 
9
- Timeout.timeout(1.5) do
9
+ Timeout.timeout(0.15) do
10
10
  w.shutdown
11
11
  end
12
12
  end
@@ -7,21 +7,21 @@ class Workhorse::PoolTest < WorkhorseTest
7
7
 
8
8
  4.times do |_i|
9
9
  p.post do
10
- sleep 0.5
10
+ sleep 0.2
11
11
  end
12
12
  end
13
13
 
14
14
  sleep 0.1
15
15
  assert_equal 1, p.idle
16
16
 
17
- sleep 0.5
17
+ sleep 0.2
18
18
  assert_equal 5, p.idle
19
19
  end
20
20
  end
21
21
 
22
22
  def test_overflow
23
23
  with_pool 5 do |p|
24
- 5.times { p.post { sleep 0.5 } }
24
+ 5.times { p.post { sleep 0.2 } }
25
25
 
26
26
  exception = assert_raises do
27
27
  p.post { sleep 1 }
@@ -2,16 +2,16 @@ require 'test_helper'
2
2
 
3
3
  class Workhorse::WorkerTest < WorkhorseTest
4
4
  def test_idle
5
- with_worker(pool_size: 5, polling_interval: 1) do |w|
5
+ with_worker(pool_size: 5, polling_interval: 0.2) do |w|
6
6
  assert_equal 5, w.idle
7
7
 
8
- sleep 0.5
9
- Workhorse.enqueue BasicJob.new(sleep_time: 1)
8
+ sleep 0.1
9
+ Workhorse.enqueue BasicJob.new(sleep_time: 0.2)
10
10
 
11
- sleep 1
11
+ sleep 0.2
12
12
  assert_equal 4, w.idle
13
13
 
14
- sleep 1
14
+ sleep 0.2
15
15
  assert_equal 5, w.idle
16
16
  end
17
17
  end
@@ -30,12 +30,12 @@ class Workhorse::WorkerTest < WorkhorseTest
30
30
  end
31
31
 
32
32
  def test_perform
33
- with_worker(polling_interval: 1) do
33
+ with_worker(polling_interval: 0.2) do
34
34
  sleep 0.1
35
35
  Workhorse.enqueue BasicJob.new(sleep_time: 0.1)
36
36
  assert_equal 'waiting', Workhorse::DbJob.first.state
37
37
 
38
- sleep 1
38
+ sleep 0.3
39
39
  end
40
40
 
41
41
  assert_equal 'succeeded', Workhorse::DbJob.first.state
@@ -54,24 +54,24 @@ class Workhorse::WorkerTest < WorkhorseTest
54
54
  end
55
55
 
56
56
  def test_term
57
- with_worker do |w|
57
+ with_worker(polling_interval: 0.2) do |w|
58
58
  Process.kill 'TERM', Process.pid
59
- sleep 1
59
+ sleep 0.2
60
60
  w.assert_state! :shutdown
61
61
  end
62
62
  end
63
63
 
64
64
  def test_int
65
- with_worker do |w|
65
+ with_worker(polling_interval: 0.2) do |w|
66
66
  Process.kill 'INT', Process.pid
67
- sleep 1
67
+ sleep 0.2
68
68
  w.assert_state! :shutdown
69
69
  end
70
70
  end
71
71
 
72
72
  def test_no_queues
73
73
  enqueue_in_multiple_queues
74
- work 1
74
+ work 0.2, polling_interval: 0.2
75
75
 
76
76
  jobs = Workhorse::DbJob.order(queue: :asc).to_a
77
77
  assert_equal 'succeeded', jobs[0].state
@@ -81,7 +81,7 @@ class Workhorse::WorkerTest < WorkhorseTest
81
81
 
82
82
  def test_nil_queue
83
83
  enqueue_in_multiple_queues
84
- work 1, queues: [nil]
84
+ work 0.2, queues: [nil], polling_interval: 0.2
85
85
 
86
86
  jobs = Workhorse::DbJob.order(queue: :asc).to_a
87
87
  assert_equal 'succeeded', jobs[0].state
@@ -91,7 +91,7 @@ class Workhorse::WorkerTest < WorkhorseTest
91
91
 
92
92
  def test_queues_with_nil
93
93
  enqueue_in_multiple_queues
94
- work 1, queues: [nil, :q1]
94
+ work 0.2, queues: [nil, :q1], polling_interval: 0.2
95
95
 
96
96
  jobs = Workhorse::DbJob.order(queue: :asc).to_a
97
97
  assert_equal 'succeeded', jobs[0].state
@@ -101,7 +101,7 @@ class Workhorse::WorkerTest < WorkhorseTest
101
101
 
102
102
  def test_queues_without_nil
103
103
  enqueue_in_multiple_queues
104
- work 1, queues: %i[q1 q2]
104
+ work 0.2, queues: %i[q1 q2], polling_interval: 0.2
105
105
 
106
106
  jobs = Workhorse::DbJob.order(queue: :asc).to_a
107
107
  assert_equal 'waiting', jobs[0].state
@@ -109,6 +109,33 @@ class Workhorse::WorkerTest < WorkhorseTest
109
109
  assert_equal 'succeeded', jobs[2].state
110
110
  end
111
111
 
112
+ def test_multiple_queued_same_queue
113
+ # One queue
114
+ Workhorse.enqueue BasicJob.new(sleep_time: 0.2), queue: :q1
115
+ Workhorse.enqueue BasicJob.new(sleep_time: 0.2), queue: :q1
116
+
117
+ work 0.2, polling_interval: 0.2
118
+
119
+ jobs = Workhorse::DbJob.all.to_a
120
+ assert_equal 'succeeded', jobs[0].state
121
+ assert_equal 'waiting', jobs[1].state
122
+
123
+ # Two queues
124
+ Workhorse::DbJob.delete_all
125
+ Workhorse.enqueue BasicJob.new(sleep_time: 0.2), queue: :q1
126
+ Workhorse.enqueue BasicJob.new(sleep_time: 0.2), queue: :q1
127
+ Workhorse.enqueue BasicJob.new(sleep_time: 0.2), queue: :q2
128
+ Workhorse.enqueue BasicJob.new(sleep_time: 0.2), queue: :q2
129
+
130
+ work 0.2, polling_interval: 0.2
131
+
132
+ jobs = Workhorse::DbJob.order(queue: :asc).to_a
133
+ assert_equal 'succeeded', jobs[0].state
134
+ assert_equal 'waiting', jobs[1].state
135
+ assert_equal 'succeeded', jobs[2].state
136
+ assert_equal 'waiting', jobs[3].state
137
+ end
138
+
112
139
  def test_order_with_priorities
113
140
  Workhorse.enqueue BasicJob.new(some_param: 6, sleep_time: 0), priority: 4
114
141
  Workhorse.enqueue BasicJob.new(some_param: 4, sleep_time: 0), priority: 3
@@ -118,10 +145,29 @@ class Workhorse::WorkerTest < WorkhorseTest
118
145
  Workhorse.enqueue BasicJob.new(some_param: 1, sleep_time: 0), priority: 0
119
146
 
120
147
  BasicJob.results.clear
121
- work 6.5, pool_size: 1
148
+ work 1.3, pool_size: 1, polling_interval: 0.2
122
149
  assert_equal (1..6).to_a, BasicJob.results
123
150
  end
124
151
 
152
+ def test_polling_interval
153
+ w = Workhorse::Worker.new(polling_interval: 1)
154
+ w = Workhorse::Worker.new(polling_interval: 1.1)
155
+ err = assert_raises do
156
+ w = Workhorse::Worker.new(polling_interval: 1.12)
157
+ end
158
+ assert_equal 'Polling interval must be a multiple of 0.1.', err.message
159
+ end
160
+
161
+ def test_perform_at
162
+ Workhorse.enqueue BasicJob.new(sleep_time: 0), perform_at: Time.now
163
+ Workhorse.enqueue BasicJob.new(sleep_time: 0), perform_at: Time.now + 600
164
+ work 0.1, polling_interval: 0.1
165
+
166
+ jobs = Workhorse::DbJob.all.to_a
167
+ assert_equal 'succeeded', jobs[0].state
168
+ assert_equal 'waiting', jobs[1].state
169
+ end
170
+
125
171
  private
126
172
 
127
173
  def enqueue_in_multiple_queues
data/workhorse.gemspec CHANGED
@@ -1,16 +1,16 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: workhorse 0.2.0 ruby lib
2
+ # stub: workhorse 0.3.0 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "workhorse".freeze
6
- s.version = "0.2.0"
6
+ s.version = "0.3.0"
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 = "2017-12-19"
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/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/run_rails_op.rb".freeze, "lib/workhorse/performer.rb".freeze, "lib/workhorse/poller.rb".freeze, "lib/workhorse/pool.rb".freeze, "lib/workhorse/worker.rb".freeze, "test/lib/db_schema.rb".freeze, "test/lib/jobs.rb".freeze, "test/lib/test_helper.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
- s.rubygems_version = "2.6.14".freeze
11
+ s.date = "2017-12-27"
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/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_rails_op.rb".freeze, "lib/workhorse/performer.rb".freeze, "lib/workhorse/poller.rb".freeze, "lib/workhorse/pool.rb".freeze, "lib/workhorse/worker.rb".freeze, "test/lib/db_schema.rb".freeze, "test/lib/jobs.rb".freeze, "test/lib/test_helper.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
+ s.rubygems_version = "2.7.2".freeze
14
14
  s.summary = "Multi-threaded job backend with database queuing for ruby.".freeze
15
15
  s.test_files = ["test/lib/db_schema.rb".freeze, "test/lib/jobs.rb".freeze, "test/lib/test_helper.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]
16
16
 
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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sitrox
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-12-19 00:00:00.000000000 Z
11
+ date: 2017-12-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -178,6 +178,7 @@ files:
178
178
  - lib/workhorse/daemon/shell_handler.rb
179
179
  - lib/workhorse/db_job.rb
180
180
  - lib/workhorse/enqueuer.rb
181
+ - lib/workhorse/jobs/cleanup_succeeded_jobs.rb
181
182
  - lib/workhorse/jobs/run_rails_op.rb
182
183
  - lib/workhorse/performer.rb
183
184
  - lib/workhorse/poller.rb