gush 1.1.1 → 2.0.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 +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
|