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 +4 -4
- data/CHANGELOG.md +20 -2
- data/FAQ.md +6 -0
- data/README.md +90 -11
- data/VERSION +1 -1
- data/lib/generators/workhorse/templates/create_table_jobs.rb +15 -2
- data/lib/workhorse.rb +1 -0
- data/lib/workhorse/daemon/shell_handler.rb +22 -0
- data/lib/workhorse/enqueuer.rb +2 -1
- data/lib/workhorse/jobs/cleanup_succeeded_jobs.rb +24 -0
- data/lib/workhorse/poller.rb +106 -21
- data/lib/workhorse/worker.rb +5 -4
- data/test/lib/db_schema.rb +5 -0
- data/test/workhorse/enqueuer_test.rb +2 -2
- data/test/workhorse/performer_test.rb +2 -4
- data/test/workhorse/poller_test.rb +2 -2
- data/test/workhorse/pool_test.rb +3 -3
- data/test/workhorse/worker_test.rb +62 -16
- data/workhorse.gemspec +5 -5
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: da01b73fc0a4be6598393d2c4b0fbbd917c83233
|
4
|
+
data.tar.gz: 637033b135c6f8fb1c4ad178c05954ba3d495a22
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
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
|
19
|
-
|
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
|
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 "
|
39
|
-
[FAQ](FAQ.md
|
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
|
-
|
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.
|
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
|
-
|
23
|
-
|
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,
|
data/lib/workhorse/enqueuer.rb
CHANGED
@@ -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
|
data/lib/workhorse/poller.rb
CHANGED
@@ -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
|
-
|
92
|
-
|
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
|
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
|
-
#
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
data/lib/workhorse/worker.rb
CHANGED
@@ -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
|
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:
|
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
|
-
|
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
|
data/test/lib/db_schema.rb
CHANGED
@@ -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
|
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
|
-
|
12
|
-
|
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
|
data/test/workhorse/pool_test.rb
CHANGED
@@ -7,21 +7,21 @@ class Workhorse::PoolTest < WorkhorseTest
|
|
7
7
|
|
8
8
|
4.times do |_i|
|
9
9
|
p.post do
|
10
|
-
sleep 0.
|
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.
|
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.
|
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:
|
5
|
+
with_worker(pool_size: 5, polling_interval: 0.2) do |w|
|
6
6
|
assert_equal 5, w.idle
|
7
7
|
|
8
|
-
sleep 0.
|
9
|
-
Workhorse.enqueue BasicJob.new(sleep_time:
|
8
|
+
sleep 0.1
|
9
|
+
Workhorse.enqueue BasicJob.new(sleep_time: 0.2)
|
10
10
|
|
11
|
-
sleep
|
11
|
+
sleep 0.2
|
12
12
|
assert_equal 4, w.idle
|
13
13
|
|
14
|
-
sleep
|
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:
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
+
# 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.
|
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-
|
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.
|
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.
|
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-
|
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
|