workhorse 0.0.1 → 0.0.2
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/.gitignore +2 -1
- data/.releaser_config +3 -0
- data/.rubocop.yml +84 -0
- data/.travis.yml +11 -0
- data/README.md +156 -4
- data/Rakefile +35 -0
- data/VERSION +1 -1
- data/bin/rubocop +1 -0
- data/lib/generators/workhorse/install_generator.rb +24 -0
- data/lib/generators/workhorse/templates/bin/workhorse.rb +7 -0
- data/lib/generators/workhorse/templates/config/initializers/workhorse.rb +11 -0
- data/lib/generators/workhorse/templates/create_table_jobs.rb +23 -0
- data/lib/workhorse.rb +42 -0
- data/lib/workhorse/daemon.rb +164 -0
- data/lib/workhorse/daemon/shell_handler.rb +54 -0
- data/lib/workhorse/db_job.rb +67 -0
- data/lib/workhorse/enqueuer.rb +22 -0
- data/lib/workhorse/jobs/run_rails_op.rb +12 -0
- data/lib/workhorse/performer.rb +91 -0
- data/lib/workhorse/poller.rb +119 -0
- data/lib/workhorse/pool.rb +51 -0
- data/lib/workhorse/worker.rb +144 -0
- data/test/lib/db_schema.rb +20 -0
- data/test/lib/jobs.rb +46 -0
- data/test/lib/test_helper.rb +29 -0
- data/test/workhorse/enqueuer_test.rb +42 -0
- data/test/workhorse/performer_test.rb +18 -0
- data/test/workhorse/poller_test.rb +13 -0
- data/test/workhorse/pool_test.rb +72 -0
- data/test/workhorse/worker_test.rb +117 -0
- data/workhorse.gemspec +55 -0
- metadata +97 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cf7da416379ae17a4defae96289beca3c4dd1d58
|
4
|
+
data.tar.gz: 5b4e9cb886632f4a78982506747e1bcbdef1a7f3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c7f8b1730ada8f2613b63ee87bf570acae14fe057bfd2650ba1ea4daa479f641a99f6d77e74b2f9484cd62aedc2899a2fd5fed8b138b09b52a2191855439d884
|
7
|
+
data.tar.gz: ee19f3e7a11eafda737333a7c41b6a83444e927c37033d8230adb26987c8bbd60c17346da5e4abfa61521500991b9856dc5fba91f5f37368fc72941a8fce60d6
|
data/.gitignore
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
*.gem
|
2
2
|
*.rbc
|
3
|
+
*.log
|
3
4
|
/.config
|
4
5
|
/coverage/
|
5
6
|
/InstalledFiles
|
@@ -42,7 +43,7 @@ build-iPhoneSimulator/
|
|
42
43
|
|
43
44
|
# for a library or gem, you might want to ignore these files since the code is
|
44
45
|
# intended to run in multiple environments; otherwise, check them in:
|
45
|
-
|
46
|
+
Gemfile.lock
|
46
47
|
# .ruby-version
|
47
48
|
# .ruby-gemset
|
48
49
|
|
data/.releaser_config
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.3
|
3
|
+
|
4
|
+
Exclude:
|
5
|
+
- 'vendor/**/*'
|
6
|
+
- 'tmp/**/*'
|
7
|
+
- 'log/**/*'
|
8
|
+
- '*.gemspec'
|
9
|
+
|
10
|
+
DisplayCopNames: true
|
11
|
+
|
12
|
+
Style/FrozenStringLiteralComment:
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
Style/DoubleNegation:
|
16
|
+
Enabled: false
|
17
|
+
|
18
|
+
Style/SignalException:
|
19
|
+
EnforcedStyle: only_fail
|
20
|
+
|
21
|
+
Lint/RescueWithoutErrorClass:
|
22
|
+
Enabled: False
|
23
|
+
|
24
|
+
Style/ConditionalAssignment:
|
25
|
+
Enabled: false
|
26
|
+
|
27
|
+
Layout/IndentArray:
|
28
|
+
EnforcedStyle: consistent
|
29
|
+
|
30
|
+
Metrics/MethodLength:
|
31
|
+
Enabled: false
|
32
|
+
|
33
|
+
Metrics/ClassLength:
|
34
|
+
Enabled: false
|
35
|
+
|
36
|
+
Metrics/ModuleLength:
|
37
|
+
Enabled: false
|
38
|
+
|
39
|
+
Metrics/ParameterLists:
|
40
|
+
Max: 5
|
41
|
+
CountKeywordArgs: false
|
42
|
+
|
43
|
+
Metrics/AbcSize:
|
44
|
+
Enabled: False
|
45
|
+
|
46
|
+
Metrics/CyclomaticComplexity:
|
47
|
+
Enabled: False
|
48
|
+
|
49
|
+
Metrics/PerceivedComplexity:
|
50
|
+
Enabled: False
|
51
|
+
|
52
|
+
Metrics/LineLength:
|
53
|
+
Max: 160
|
54
|
+
|
55
|
+
Metrics/BlockNesting:
|
56
|
+
Enabled: false
|
57
|
+
|
58
|
+
Metrics/BlockLength:
|
59
|
+
Enabled: false
|
60
|
+
|
61
|
+
Style/IfUnlessModifier:
|
62
|
+
Enabled: false
|
63
|
+
|
64
|
+
Style/Documentation:
|
65
|
+
Enabled: false
|
66
|
+
|
67
|
+
Style/RedundantReturn:
|
68
|
+
Enabled: false
|
69
|
+
|
70
|
+
Style/AsciiComments:
|
71
|
+
Enabled: false
|
72
|
+
|
73
|
+
Style/GuardClause:
|
74
|
+
Enabled: false
|
75
|
+
|
76
|
+
Style/ClassAndModuleChildren:
|
77
|
+
Enabled: false
|
78
|
+
EnforcedStyle: compact
|
79
|
+
SupportedStyles:
|
80
|
+
- nested
|
81
|
+
- compact
|
82
|
+
|
83
|
+
Style/NumericPredicate:
|
84
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,20 +1,172 @@
|
|
1
1
|
[](https://travis-ci.org/sitrox/workhorse)
|
2
2
|
[](https://badge.fury.io/rb/workhorse)
|
3
3
|
|
4
|
-
**This gem is considered work in progress. Do not use this in production yet.**
|
5
|
-
|
6
4
|
# Workhorse
|
7
5
|
|
6
|
+
**This Gem is still in an early stage of development. Please not use this in production yet.**
|
7
|
+
|
8
8
|
Multi-threaded job backend with database queuing for ruby.
|
9
9
|
|
10
|
+
## Introduction
|
11
|
+
|
12
|
+
What it is:
|
13
|
+
|
14
|
+
* Jobs are instances of classes that support the `perform` method.
|
15
|
+
* Jobs are persisted in the database using ActiveRecord.
|
16
|
+
* You can start one or more worker processes.
|
17
|
+
* Each worker is configurable as to which queue(s) it processes.
|
18
|
+
* Each worker polls the database and spawns a number of threads to execute jobs
|
19
|
+
of different queues simultaneously.
|
20
|
+
|
21
|
+
What it isn't:
|
22
|
+
|
23
|
+
* It cannot spawn new processes. Jobs are run in separate threads but not in
|
24
|
+
separate processes (unless you start multiple worker processes manually).
|
25
|
+
* It does not support retries.
|
26
|
+
|
10
27
|
## Installation
|
11
28
|
|
12
|
-
|
29
|
+
### Requirements
|
30
|
+
|
31
|
+
* A database and table handler that properly supports row-level locking (such as
|
32
|
+
MySQL with InnoDB, PostgreSQL or Oracle).
|
33
|
+
* An operating system and file system that supports file locking.
|
34
|
+
|
35
|
+
### Installing under Rails
|
36
|
+
|
37
|
+
1. Add `workhorse` to your `Gemfile`:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
gem 'workhorse'
|
41
|
+
```
|
42
|
+
|
43
|
+
2. Run the install generator:
|
44
|
+
|
45
|
+
```bash
|
46
|
+
bin/rails generate workhorse:install
|
47
|
+
```
|
48
|
+
|
49
|
+
This generates:
|
50
|
+
|
51
|
+
* A database migration for creating the `jobs` table
|
52
|
+
* An initializer `config/initializers/workhorse.rb` for global configuration
|
53
|
+
* A daemon worker script under `bin/workhorse.rb`
|
54
|
+
|
55
|
+
Please customize the configuration files to your liking.
|
56
|
+
|
57
|
+
## Queuing jobs
|
58
|
+
|
59
|
+
### Basic jobs
|
60
|
+
|
61
|
+
Workhorse can handle any jobs that support the `perform` method and are
|
62
|
+
serializable. To queue a basic job, use the static method `Workhorse.enqueue`:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
class MyJob
|
66
|
+
def initialize(name)
|
67
|
+
@name = name
|
68
|
+
end
|
69
|
+
|
70
|
+
def perform
|
71
|
+
puts "Hello #{@name}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
Workhorse.enqueue MyJob.new('John'), queue: :test
|
76
|
+
```
|
77
|
+
|
78
|
+
In the above example, we also specify a queue named `:test`. This means that
|
79
|
+
this job will never run simoultaneously with other jobs in the same queue. If no
|
80
|
+
queue is given, the job can always be executed simoultaneously with any other
|
81
|
+
job.
|
82
|
+
|
83
|
+
### RailsOps operations
|
84
|
+
|
85
|
+
Workhorse allows you to easily queue
|
86
|
+
[RailsOps](https://github.com/sitrox/rails_ops) operations using the static
|
87
|
+
method `Workhorse.enqueue_op`:
|
13
88
|
|
14
89
|
```ruby
|
15
|
-
|
90
|
+
Workhorse.enqueue Operations::Jobs::CleanUpDatabase, quiet: true
|
16
91
|
```
|
17
92
|
|
93
|
+
Params passed using the second argument will be used for operation instantiation
|
94
|
+
at job execution.
|
95
|
+
|
96
|
+
You can also specify a queue:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
Workhorse.enqueue Operations::Jobs::CleanUpDatabase, { quiet: true }, queue: :maintenance
|
100
|
+
```
|
101
|
+
|
102
|
+
## Configuring and starting workers
|
103
|
+
|
104
|
+
Workers poll the database for new jobs and execute them in one or more threads.
|
105
|
+
Typically, one worker is started per process. While you can start workers
|
106
|
+
manually, either in your main application process(es) or in a separate one,
|
107
|
+
workhorse also provides you with a convenient way of starting one or multiple
|
108
|
+
worker processes as a daemon.
|
109
|
+
|
110
|
+
### Start workers manually
|
111
|
+
|
112
|
+
Workers are created by instantiatating, configuring and starting a new
|
113
|
+
`Workhorse::Worker` instance.
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
Workhorse::Worker.start_and_wait(
|
117
|
+
pool_size: 5, # Processes 5 jobs concurrently
|
118
|
+
quiet: false, # Logs to STDOUT
|
119
|
+
logger: Rails.logger # Logs to Rails log. You can also
|
120
|
+
# provide any custom logger.
|
121
|
+
)
|
122
|
+
```
|
123
|
+
|
124
|
+
See [code
|
125
|
+
documentation](http://www.rubydoc.info/github/sitrox/workhorse/Workhorse%2FWorker:initialize)
|
126
|
+
for more information on the arguments.
|
127
|
+
|
128
|
+
### Start workers using a daemon script
|
129
|
+
|
130
|
+
Using `Workhorse::Daemon` (`Workhorse::Daemon::ShellHandler`), you can spawn one
|
131
|
+
or multiple worker processes automatically. This is useful for cases where you
|
132
|
+
want the workers to exist in separate processes as opposed to in your main
|
133
|
+
application process(es).
|
134
|
+
|
135
|
+
For this case, the workhorse install routine automatically creates a file called
|
136
|
+
`bin/workhorse.rb` which can be used to start one or more worker processes.
|
137
|
+
|
138
|
+
To start the daemon:
|
139
|
+
|
140
|
+
```bash
|
141
|
+
bin/workhorse.rb start[|stop|status|watch|restart|usage]
|
142
|
+
```
|
143
|
+
|
144
|
+
#### Background and customization
|
145
|
+
|
146
|
+
The daemon-part allows you to run arbitrary code as a daemon:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
Workhorse::Daemon::ShellHandler.run count: 5 do
|
150
|
+
# This runs as a daemon and will be started 5 times
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
Within this shell handler, you can now instantiate, configure and start a worker
|
155
|
+
as described under *Start workers manually*:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
Workhorse::Daemon::ShellHandler.run count: 5 do
|
159
|
+
# This will be run 5 times, each time in a separate process. Per process, it
|
160
|
+
# will be able to process 3 jobs concurrently.
|
161
|
+
Workhorse::Worker.start_and_wait(pool_size: 3, logger: Rails.logger)
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
## Roadmap
|
166
|
+
|
167
|
+
* [ ] ActiveJob integration for Rails
|
168
|
+
* [ ] Job timeouts
|
169
|
+
|
18
170
|
## Copyright
|
19
171
|
|
20
172
|
Copyright (c) 2017 Sitrox. See `LICENSE` for further details.
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
task :gemspec do
|
2
|
+
gemspec = Gem::Specification.new do |spec|
|
3
|
+
spec.name = 'workhorse'
|
4
|
+
spec.version = IO.read('VERSION').chomp
|
5
|
+
spec.authors = ['Sitrox']
|
6
|
+
spec.summary = %(
|
7
|
+
Multi-threaded job backend with database queuing for ruby.
|
8
|
+
)
|
9
|
+
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
10
|
+
spec.executables = []
|
11
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
12
|
+
spec.require_paths = ['lib']
|
13
|
+
|
14
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
15
|
+
spec.add_development_dependency 'rake'
|
16
|
+
spec.add_development_dependency 'rubocop', '0.51.0'
|
17
|
+
spec.add_development_dependency 'minitest'
|
18
|
+
spec.add_development_dependency 'mysql2', '~> 0.3.13'
|
19
|
+
spec.add_development_dependency 'benchmark-ips'
|
20
|
+
spec.add_dependency 'activesupport'
|
21
|
+
spec.add_dependency 'activerecord'
|
22
|
+
spec.add_dependency 'schemacop', '~> 2.0'
|
23
|
+
spec.add_dependency 'concurrent-ruby'
|
24
|
+
end
|
25
|
+
|
26
|
+
File.open('workhorse.gemspec', 'w') { |f| f.write(gemspec.to_ruby.strip) }
|
27
|
+
end
|
28
|
+
|
29
|
+
require 'rake/testtask'
|
30
|
+
|
31
|
+
Rake::TestTask.new do |t|
|
32
|
+
t.pattern = 'test/workhorse/**/*_test.rb'
|
33
|
+
t.verbose = false
|
34
|
+
t.libs << 'test/lib'
|
35
|
+
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.2
|
data/bin/rubocop
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
bundle exec rubocop "$@"
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Workhorse
|
2
|
+
class InstallGenerator < Rails::Generators::Base
|
3
|
+
include Rails::Generators::Migration
|
4
|
+
|
5
|
+
source_root File.expand_path('../templates', __FILE__)
|
6
|
+
|
7
|
+
def self.next_migration_number(_dir)
|
8
|
+
Time.now.utc.strftime('%Y%m%d%H%M%S')
|
9
|
+
end
|
10
|
+
|
11
|
+
def install_migration
|
12
|
+
migration_template 'create_table_jobs.rb', 'db/migrate/create_table_jobs.rb'
|
13
|
+
end
|
14
|
+
|
15
|
+
def install_daemon_script
|
16
|
+
template 'bin/workhorse.rb'
|
17
|
+
chmod 'bin/workhorse.rb', 0o755
|
18
|
+
end
|
19
|
+
|
20
|
+
def install_initializer
|
21
|
+
template 'config/initializers/workhorse.rb'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
Workhorse.setup do |config|
|
2
|
+
# Set this to false in order to prevent jobs from being automatically
|
3
|
+
# wrapped into a transaction. The built-in workhorse logic will still run
|
4
|
+
# in transactions.
|
5
|
+
config.perform_jobs_in_tx = true
|
6
|
+
|
7
|
+
# Enable this to specify an alternative callback for handling transactions.
|
8
|
+
# config.tx_callback = proc do |&block|
|
9
|
+
# ActiveRecord::Base.transaction&(&block)
|
10
|
+
# end
|
11
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class CreateTableJobs < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :jobs, force: true do |t|
|
4
|
+
t.string :state, null: false, default: 'waiting'
|
5
|
+
t.string :queue, null: true
|
6
|
+
t.text :handler, null: false, limit: 4_294_967_295
|
7
|
+
|
8
|
+
t.string :locked_by
|
9
|
+
t.datetime :locked_at
|
10
|
+
|
11
|
+
t.datetime :started_at
|
12
|
+
|
13
|
+
t.datetime :succeeded_at
|
14
|
+
t.datetime :failed_at
|
15
|
+
t.text :last_error, limit: 4_294_967_295
|
16
|
+
|
17
|
+
t.timestamps null: false
|
18
|
+
end
|
19
|
+
|
20
|
+
add_index :jobs, :queue
|
21
|
+
add_index :jobs, :state
|
22
|
+
end
|
23
|
+
end
|
data/lib/workhorse.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'active_support/all'
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
module Workhorse
|
6
|
+
@set_up = false
|
7
|
+
|
8
|
+
# Returns the performer currently performing the active job. This can only be
|
9
|
+
# called from within a job and the same thread.
|
10
|
+
def self.performer
|
11
|
+
Thread.current[:workhorse_current_performer]\
|
12
|
+
|| fail('No performer is associated with the current thread. This method must always be called inside of a job.')
|
13
|
+
end
|
14
|
+
|
15
|
+
cattr_accessor :tx_callback
|
16
|
+
self.tx_callback = proc do |&block|
|
17
|
+
ActiveRecord::Base.transaction(&block)
|
18
|
+
end
|
19
|
+
|
20
|
+
cattr_accessor :perform_jobs_in_tx
|
21
|
+
self.perform_jobs_in_tx = true
|
22
|
+
|
23
|
+
def self.setup
|
24
|
+
fail 'Workhorse is already set up.' if @set_up
|
25
|
+
yield self
|
26
|
+
@set_up = true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
require 'workhorse/db_job'
|
31
|
+
require 'workhorse/enqueuer'
|
32
|
+
require 'workhorse/performer'
|
33
|
+
require 'workhorse/poller'
|
34
|
+
require 'workhorse/pool'
|
35
|
+
require 'workhorse/worker'
|
36
|
+
require 'workhorse/jobs/run_rails_op'
|
37
|
+
|
38
|
+
# Daemon functionality is not available on java platforms
|
39
|
+
if RUBY_PLATFORM != 'java'
|
40
|
+
require 'workhorse/daemon'
|
41
|
+
require 'workhorse/daemon/shell_handler'
|
42
|
+
end
|