gush 1.1.1 → 2.0.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 +12 -0
- data/gush.gemspec +2 -1
- data/lib/gush/client.rb +46 -23
- data/lib/gush/job.rb +12 -6
- data/lib/gush/worker.rb +7 -3
- data/lib/gush/workflow.rb +8 -3
- data/spec/features/integration_spec.rb +41 -0
- data/spec/gush/job_spec.rb +6 -4
- data/spec/gush/workflow_spec.rb +1 -0
- data/spec/spec_helper.rb +1 -2
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a9e61295e87f5c707a6f93a3cce7238c0650876be7ff76c4cba0c60133832217
|
4
|
+
data.tar.gz: 7df67c0134bc1e7bd036da850287191463557a764279790072f3f0a3887bf44b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 133913d52aaef1d470ca8be41c865c26949577df06935b034e83bb0ee616f5cd812d0c49730bbfc504342a46a6f0c6b813df19705f0a6c4448ba18969172e418
|
7
|
+
data.tar.gz: b08039f50aba1ab24fbe957669f16d51258913353ffba98b168a9df74782b46459c79f083c7e54b1dfa717a5878e85bb72ba07397993f833e83f9733ec5089dd
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## 2.0.0
|
9
|
+
|
10
|
+
## Changed
|
11
|
+
|
12
|
+
- *[BREAKING]* Store gush jobs on redis hash instead of plain keys - this improves performance when retrieving keys (Thanks to @Saicheg! [See pull request](https://github.com/chaps-io/gush/pull/56))
|
13
|
+
|
14
|
+
|
15
|
+
## Added
|
16
|
+
|
17
|
+
- Allow setting queue for each job via `:queue` option in `run` method (Thanks to @devilankur18! [See pull request](https://github.com/chaps-io/gush/pull/58))
|
18
|
+
|
19
|
+
|
8
20
|
## 1.1.1 - 2018-06-09
|
9
21
|
|
10
22
|
## Changed
|
data/gush.gemspec
CHANGED
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
6
|
spec.name = "gush"
|
7
|
-
spec.version = "
|
7
|
+
spec.version = "2.0.0"
|
8
8
|
spec.authors = ["Piotrek Okoński"]
|
9
9
|
spec.email = ["piotrek@okonski.org"]
|
10
10
|
spec.summary = "Fast and distributed workflow runner based on ActiveJob and Redis"
|
@@ -21,6 +21,7 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.add_dependency "connection_pool", "~> 2.2.1"
|
22
22
|
spec.add_dependency "multi_json", "~> 1.11"
|
23
23
|
spec.add_dependency "redis", ">= 3.2", "< 5"
|
24
|
+
spec.add_dependency "redis-mutex", "~> 4.0.1"
|
24
25
|
spec.add_dependency "hiredis", "~> 0.6"
|
25
26
|
spec.add_dependency "ruby-graphviz", "~> 1.2"
|
26
27
|
spec.add_dependency "terminal-table", "~> 1.4"
|
data/lib/gush/client.rb
CHANGED
@@ -42,19 +42,19 @@ module Gush
|
|
42
42
|
persist_workflow(workflow)
|
43
43
|
end
|
44
44
|
|
45
|
-
def next_free_job_id(workflow_id,job_klass)
|
46
|
-
|
45
|
+
def next_free_job_id(workflow_id, job_klass)
|
46
|
+
job_id = nil
|
47
|
+
|
47
48
|
loop do
|
48
|
-
|
49
|
-
job_identifier = "#{job_klass}-#{id}"
|
49
|
+
job_id = SecureRandom.uuid
|
50
50
|
available = connection_pool.with do |redis|
|
51
|
-
!redis.
|
51
|
+
!redis.hexists("gush.jobs.#{workflow_id}.#{job_klass}", job_id)
|
52
52
|
end
|
53
53
|
|
54
54
|
break if available
|
55
55
|
end
|
56
56
|
|
57
|
-
|
57
|
+
job_id
|
58
58
|
end
|
59
59
|
|
60
60
|
def next_free_workflow_id
|
@@ -87,7 +87,11 @@ module Gush
|
|
87
87
|
unless data.nil?
|
88
88
|
hash = Gush::JSON.decode(data, symbolize_keys: true)
|
89
89
|
keys = redis.scan_each(match: "gush.jobs.#{id}.*")
|
90
|
-
|
90
|
+
|
91
|
+
nodes = keys.each_with_object([]) do |key, array|
|
92
|
+
array.concat redis.hvals(key).map { |json| Gush::JSON.decode(json, symbolize_keys: true) }
|
93
|
+
end
|
94
|
+
|
91
95
|
workflow_from_hash(hash, nodes)
|
92
96
|
else
|
93
97
|
raise WorkflowNotFound.new("Workflow with given id doesn't exist")
|
@@ -102,28 +106,24 @@ module Gush
|
|
102
106
|
|
103
107
|
workflow.jobs.each {|job| persist_job(workflow.id, job) }
|
104
108
|
workflow.mark_as_persisted
|
109
|
+
|
105
110
|
true
|
106
111
|
end
|
107
112
|
|
108
113
|
def persist_job(workflow_id, job)
|
109
114
|
connection_pool.with do |redis|
|
110
|
-
redis.
|
115
|
+
redis.hset("gush.jobs.#{workflow_id}.#{job.klass}", job.id, job.to_json)
|
111
116
|
end
|
112
117
|
end
|
113
118
|
|
114
|
-
def find_job(workflow_id,
|
115
|
-
job_name_match = /(?<klass>\w*[^-])-(?<identifier>.*)/.match(
|
116
|
-
hypen = '-' if job_name_match.nil?
|
117
|
-
|
118
|
-
keys = connection_pool.with do |redis|
|
119
|
-
redis.scan_each(match: "gush.jobs.#{workflow_id}.#{job_id}#{hypen}*").to_a
|
120
|
-
end
|
121
|
-
|
122
|
-
return nil if keys.nil?
|
119
|
+
def find_job(workflow_id, job_name)
|
120
|
+
job_name_match = /(?<klass>\w*[^-])-(?<identifier>.*)/.match(job_name)
|
123
121
|
|
124
|
-
data =
|
125
|
-
|
126
|
-
|
122
|
+
data = if job_name_match
|
123
|
+
find_job_by_klass_and_id(workflow_id, job_name)
|
124
|
+
else
|
125
|
+
find_job_by_klass(workflow_id, job_name)
|
126
|
+
end
|
127
127
|
|
128
128
|
return nil if data.nil?
|
129
129
|
|
@@ -140,7 +140,7 @@ module Gush
|
|
140
140
|
|
141
141
|
def destroy_job(workflow_id, job)
|
142
142
|
connection_pool.with do |redis|
|
143
|
-
redis.del("gush.jobs.#{workflow_id}.#{job.
|
143
|
+
redis.del("gush.jobs.#{workflow_id}.#{job.klass}")
|
144
144
|
end
|
145
145
|
end
|
146
146
|
|
@@ -162,12 +162,33 @@ module Gush
|
|
162
162
|
def enqueue_job(workflow_id, job)
|
163
163
|
job.enqueue!
|
164
164
|
persist_job(workflow_id, job)
|
165
|
+
queue = job.queue || configuration.namespace
|
165
166
|
|
166
|
-
Gush::Worker.set(queue:
|
167
|
+
Gush::Worker.set(queue: queue).perform_later(*[workflow_id, job.name])
|
167
168
|
end
|
168
169
|
|
169
170
|
private
|
170
171
|
|
172
|
+
def find_job_by_klass_and_id(workflow_id, job_name)
|
173
|
+
job_klass, job_id = job_name.split('|')
|
174
|
+
|
175
|
+
connection_pool.with do |redis|
|
176
|
+
redis.hget("gush.jobs.#{workflow_id}.#{job_klass}", job_id)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def find_job_by_klass(workflow_id, job_name)
|
181
|
+
new_cursor, result = connection_pool.with do |redis|
|
182
|
+
redis.hscan("gush.jobs.#{workflow_id}.#{job_name}", 0, count: 1)
|
183
|
+
end
|
184
|
+
|
185
|
+
return nil if result.empty?
|
186
|
+
|
187
|
+
job_id, job = *result[0]
|
188
|
+
|
189
|
+
job
|
190
|
+
end
|
191
|
+
|
171
192
|
def workflow_from_hash(hash, nodes = [])
|
172
193
|
flow = hash[:klass].constantize.new(*hash[:arguments])
|
173
194
|
flow.jobs = []
|
@@ -182,7 +203,9 @@ module Gush
|
|
182
203
|
end
|
183
204
|
|
184
205
|
def build_redis
|
185
|
-
Redis.new(url: configuration.redis_url)
|
206
|
+
Redis.new(url: configuration.redis_url).tap do |instance|
|
207
|
+
RedisClassy.redis = instance
|
208
|
+
end
|
186
209
|
end
|
187
210
|
|
188
211
|
def connection_pool
|
data/lib/gush/job.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module Gush
|
2
2
|
class Job
|
3
3
|
attr_accessor :workflow_id, :incoming, :outgoing, :params,
|
4
|
-
:finished_at, :failed_at, :started_at, :enqueued_at, :payloads, :klass
|
5
|
-
attr_reader :
|
4
|
+
:finished_at, :failed_at, :started_at, :enqueued_at, :payloads, :klass, :queue
|
5
|
+
attr_reader :id, :klass, :output_payload, :params
|
6
6
|
|
7
7
|
def initialize(opts = {})
|
8
8
|
options = opts.dup
|
@@ -11,8 +11,9 @@ module Gush
|
|
11
11
|
|
12
12
|
def as_json
|
13
13
|
{
|
14
|
-
|
15
|
-
klass:
|
14
|
+
id: id,
|
15
|
+
klass: klass.to_s,
|
16
|
+
queue: queue,
|
16
17
|
incoming: incoming,
|
17
18
|
outgoing: outgoing,
|
18
19
|
finished_at: finished_at,
|
@@ -25,6 +26,10 @@ module Gush
|
|
25
26
|
}
|
26
27
|
end
|
27
28
|
|
29
|
+
def name
|
30
|
+
@name ||= "#{klass}|#{id}"
|
31
|
+
end
|
32
|
+
|
28
33
|
def to_json(options = {})
|
29
34
|
Gush::JSON.encode(as_json)
|
30
35
|
end
|
@@ -108,7 +113,7 @@ module Gush
|
|
108
113
|
end
|
109
114
|
|
110
115
|
def assign_variables(opts)
|
111
|
-
@
|
116
|
+
@id = opts[:id]
|
112
117
|
@incoming = opts[:incoming] || []
|
113
118
|
@outgoing = opts[:outgoing] || []
|
114
119
|
@failed_at = opts[:failed_at]
|
@@ -116,9 +121,10 @@ module Gush
|
|
116
121
|
@started_at = opts[:started_at]
|
117
122
|
@enqueued_at = opts[:enqueued_at]
|
118
123
|
@params = opts[:params] || {}
|
119
|
-
@klass = opts[:klass]
|
124
|
+
@klass = opts[:klass] || self.class
|
120
125
|
@output_payload = opts[:output_payload]
|
121
126
|
@workflow_id = opts[:workflow_id]
|
127
|
+
@queue = opts[:queue]
|
122
128
|
end
|
123
129
|
end
|
124
130
|
end
|
data/lib/gush/worker.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'active_job'
|
2
|
+
require 'redis-mutex'
|
2
3
|
|
3
4
|
module Gush
|
4
5
|
class Worker < ::ActiveJob::Base
|
@@ -66,9 +67,12 @@ module Gush
|
|
66
67
|
|
67
68
|
def enqueue_outgoing_jobs
|
68
69
|
job.outgoing.each do |job_name|
|
69
|
-
|
70
|
-
|
71
|
-
|
70
|
+
RedisMutex.with_lock("gush_enqueue_outgoing_jobs_#{workflow_id}-#{job_name}", sleep: 0.3, block: 2) do
|
71
|
+
out = client.find_job(workflow_id, job_name)
|
72
|
+
|
73
|
+
if out.ready_to_start?
|
74
|
+
client.enqueue_job(workflow_id, out)
|
75
|
+
end
|
72
76
|
end
|
73
77
|
end
|
74
78
|
end
|
data/lib/gush/workflow.rb
CHANGED
@@ -77,11 +77,13 @@ module Gush
|
|
77
77
|
|
78
78
|
def find_job(name)
|
79
79
|
match_data = /(?<klass>\w*[^-])-(?<identifier>.*)/.match(name.to_s)
|
80
|
+
|
80
81
|
if match_data.nil?
|
81
|
-
job = jobs.find { |node| node.
|
82
|
+
job = jobs.find { |node| node.klass.to_s == name.to_s }
|
82
83
|
else
|
83
84
|
job = jobs.find { |node| node.name.to_s == name.to_s }
|
84
85
|
end
|
86
|
+
|
85
87
|
job
|
86
88
|
end
|
87
89
|
|
@@ -108,18 +110,21 @@ module Gush
|
|
108
110
|
def run(klass, opts = {})
|
109
111
|
node = klass.new({
|
110
112
|
workflow_id: id,
|
111
|
-
|
112
|
-
params: opts.fetch(:params, {})
|
113
|
+
id: client.next_free_job_id(id, klass.to_s),
|
114
|
+
params: opts.fetch(:params, {}),
|
115
|
+
queue: opts[:queue]
|
113
116
|
})
|
114
117
|
|
115
118
|
jobs << node
|
116
119
|
|
117
120
|
deps_after = [*opts[:after]]
|
121
|
+
|
118
122
|
deps_after.each do |dep|
|
119
123
|
@dependencies << {from: dep.to_s, to: node.name.to_s }
|
120
124
|
end
|
121
125
|
|
122
126
|
deps_before = [*opts[:before]]
|
127
|
+
|
123
128
|
deps_before.each do |dep|
|
124
129
|
@dependencies << {from: node.name.to_s, to: dep.to_s }
|
125
130
|
end
|
@@ -158,4 +158,45 @@ describe "Workflows" do
|
|
158
158
|
expect(flow).to be_finished
|
159
159
|
expect(flow).to_not be_failed
|
160
160
|
end
|
161
|
+
|
162
|
+
it 'executes job with multiple ancestors only once' do
|
163
|
+
NO_DUPS_INTERNAL_SPY = double('spy')
|
164
|
+
expect(NO_DUPS_INTERNAL_SPY).to receive(:some_method).exactly(1).times
|
165
|
+
|
166
|
+
class FirstAncestor < Gush::Job
|
167
|
+
def perform
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
class SecondAncestor < Gush::Job
|
172
|
+
def perform
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
class FinalJob < Gush::Job
|
177
|
+
def perform
|
178
|
+
NO_DUPS_INTERNAL_SPY.some_method
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
class NoDuplicatesWorkflow < Gush::Workflow
|
183
|
+
def configure
|
184
|
+
run FirstAncestor
|
185
|
+
run SecondAncestor
|
186
|
+
|
187
|
+
run FinalJob, after: [FirstAncestor, SecondAncestor]
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
flow = NoDuplicatesWorkflow.create
|
192
|
+
flow.start!
|
193
|
+
|
194
|
+
5.times do
|
195
|
+
perform_one
|
196
|
+
end
|
197
|
+
|
198
|
+
flow = flow.reload
|
199
|
+
expect(flow).to be_finished
|
200
|
+
expect(flow).to_not be_failed
|
201
|
+
end
|
161
202
|
end
|
data/spec/gush/job_spec.rb
CHANGED
@@ -62,9 +62,9 @@ describe Gush::Job do
|
|
62
62
|
describe "#as_json" do
|
63
63
|
context "finished and enqueued set to true" do
|
64
64
|
it "returns correct hash" do
|
65
|
-
job = described_class.new(workflow_id: 123,
|
65
|
+
job = described_class.new(workflow_id: 123, id: "702bced5-bb72-4bba-8f6f-15a3afa358bd", finished_at: 123, enqueued_at: 120)
|
66
66
|
expected = {
|
67
|
-
|
67
|
+
id: '702bced5-bb72-4bba-8f6f-15a3afa358bd',
|
68
68
|
klass: "Gush::Job",
|
69
69
|
incoming: [],
|
70
70
|
outgoing: [],
|
@@ -73,6 +73,7 @@ describe Gush::Job do
|
|
73
73
|
finished_at: 123,
|
74
74
|
enqueued_at: 120,
|
75
75
|
params: {},
|
76
|
+
queue: nil,
|
76
77
|
output_payload: nil,
|
77
78
|
workflow_id: 123
|
78
79
|
}
|
@@ -86,7 +87,7 @@ describe Gush::Job do
|
|
86
87
|
job = described_class.from_hash(
|
87
88
|
{
|
88
89
|
klass: 'Gush::Job',
|
89
|
-
|
90
|
+
id: '702bced5-bb72-4bba-8f6f-15a3afa358bd',
|
90
91
|
incoming: ['a', 'b'],
|
91
92
|
outgoing: ['c'],
|
92
93
|
failed_at: 123,
|
@@ -96,7 +97,8 @@ describe Gush::Job do
|
|
96
97
|
}
|
97
98
|
)
|
98
99
|
|
99
|
-
expect(job.
|
100
|
+
expect(job.id).to eq('702bced5-bb72-4bba-8f6f-15a3afa358bd')
|
101
|
+
expect(job.name).to eq('Gush::Job|702bced5-bb72-4bba-8f6f-15a3afa358bd')
|
100
102
|
expect(job.class).to eq(Gush::Job)
|
101
103
|
expect(job.klass).to eq("Gush::Job")
|
102
104
|
expect(job.finished?).to eq(true)
|
data/spec/gush/workflow_spec.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -26,7 +26,6 @@ class TestWorkflow < Gush::Workflow
|
|
26
26
|
run FetchSecondJob, after: Prepare, before: NormalizeJob
|
27
27
|
|
28
28
|
run PersistFirstJob, after: FetchFirstJob, before: NormalizeJob
|
29
|
-
|
30
29
|
end
|
31
30
|
end
|
32
31
|
|
@@ -62,7 +61,7 @@ module GushHelpers
|
|
62
61
|
end
|
63
62
|
|
64
63
|
def job_with_id(job_name)
|
65
|
-
/#{job_name}
|
64
|
+
/#{job_name}|(?<identifier>.*)/
|
66
65
|
end
|
67
66
|
end
|
68
67
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gush
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Piotrek Okoński
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-11-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -78,6 +78,20 @@ dependencies:
|
|
78
78
|
- - "<"
|
79
79
|
- !ruby/object:Gem::Version
|
80
80
|
version: '5'
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: redis-mutex
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 4.0.1
|
88
|
+
type: :runtime
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - "~>"
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: 4.0.1
|
81
95
|
- !ruby/object:Gem::Dependency
|
82
96
|
name: hiredis
|
83
97
|
requirement: !ruby/object:Gem::Requirement
|