workhorse 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|