gush 2.0.1 → 2.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 -0
- data/.travis.yml +3 -0
- data/README.md +0 -2
- data/gush.gemspec +4 -4
- data/lib/gush/client.rb +39 -57
- data/lib/gush/worker.rb +8 -0
- data/spec/gush/client_spec.rb +8 -2
- data/spec/gush/worker_spec.rb +12 -0
- data/spec/spec_helper.rb +13 -0
- metadata +12 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 12fdb9a62d33353f827194c198c011ff42d7491669d0ab9717be0920a8066313
|
4
|
+
data.tar.gz: 7f3d4ada23215e818f0cb801c92f4752e80b21b344f3b48d9ab4b83787c029ec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bec4bcb3e251bdb1a2e184b6b62ae896fd40af4a0cd4bad2ec7ee4925ab356e36a3388d2cb78ed21582c0f6cfdd429c304f0591a5ee3760c73255d03a2b1d80e
|
7
|
+
data.tar.gz: 67fba0b65b575449114233a4ff9cfaf1cb6ac3ff61633f94b7c595988165b3315fe7ae5d0744792315d9329795da0e42ae840789e3d4f72c84ef8956081c5fbd
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# Gush [](https://travis-ci.org/chaps-io/gush)
|
2
2
|
|
3
|
-
## [](https://chaps.io) proudly made by [Chaps](https://chaps.io)
|
4
|
-
|
5
3
|
Gush is a parallel workflow runner using only Redis as storage and [ActiveJob](http://guides.rubyonrails.org/v4.2/active_job_basics.html#introduction) for scheduling and executing jobs.
|
6
4
|
|
7
5
|
## Theory
|
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 = "2.0.
|
7
|
+
spec.version = "2.0.2"
|
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"
|
@@ -17,8 +17,8 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
18
|
spec.require_paths = ["lib"]
|
19
19
|
|
20
|
-
spec.add_dependency "activejob", ">= 4.2.7", "<
|
21
|
-
spec.add_dependency "
|
20
|
+
spec.add_dependency "activejob", ">= 4.2.7", "< 7.0"
|
21
|
+
spec.add_dependency "concurrent-ruby", "~> 1.0"
|
22
22
|
spec.add_dependency "multi_json", "~> 1.11"
|
23
23
|
spec.add_dependency "redis", ">= 3.2", "< 5"
|
24
24
|
spec.add_dependency "redis-mutex", "~> 4.0.1"
|
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.add_dependency "colorize", "~> 0.7"
|
29
29
|
spec.add_dependency "thor", "~> 0.19"
|
30
30
|
spec.add_dependency "launchy", "~> 2.4"
|
31
|
-
spec.add_development_dependency "bundler"
|
31
|
+
spec.add_development_dependency "bundler"
|
32
32
|
spec.add_development_dependency "rake", "~> 10.4"
|
33
33
|
spec.add_development_dependency "rspec", '~> 3.0'
|
34
34
|
spec.add_development_dependency "pry", '~> 0.10'
|
data/lib/gush/client.rb
CHANGED
@@ -1,9 +1,22 @@
|
|
1
|
-
require '
|
1
|
+
require 'redis'
|
2
|
+
require 'concurrent-ruby'
|
2
3
|
|
3
4
|
module Gush
|
4
5
|
class Client
|
5
6
|
attr_reader :configuration
|
6
7
|
|
8
|
+
@@redis_connection = Concurrent::ThreadLocalVar.new(nil)
|
9
|
+
|
10
|
+
def self.redis_connection(config)
|
11
|
+
cached = (@@redis_connection.value ||= { url: config.redis_url, connection: nil })
|
12
|
+
return cached[:connection] if !cached[:connection].nil? && config.redis_url == cached[:url]
|
13
|
+
|
14
|
+
Redis.new(url: config.redis_url).tap do |instance|
|
15
|
+
RedisClassy.redis = instance
|
16
|
+
@@redis_connection.value = { url: config.redis_url, connection: instance }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
7
20
|
def initialize(config = Gush.configuration)
|
8
21
|
@configuration = config
|
9
22
|
end
|
@@ -47,9 +60,7 @@ module Gush
|
|
47
60
|
|
48
61
|
loop do
|
49
62
|
job_id = SecureRandom.uuid
|
50
|
-
available =
|
51
|
-
!redis.hexists("gush.jobs.#{workflow_id}.#{job_klass}", job_id)
|
52
|
-
end
|
63
|
+
available = !redis.hexists("gush.jobs.#{workflow_id}.#{job_klass}", job_id)
|
53
64
|
|
54
65
|
break if available
|
55
66
|
end
|
@@ -61,9 +72,7 @@ module Gush
|
|
61
72
|
id = nil
|
62
73
|
loop do
|
63
74
|
id = SecureRandom.uuid
|
64
|
-
available =
|
65
|
-
!redis.exists("gush.workflow.#{id}")
|
66
|
-
end
|
75
|
+
available = !redis.exists("gush.workflow.#{id}")
|
67
76
|
|
68
77
|
break if available
|
69
78
|
end
|
@@ -72,37 +81,31 @@ module Gush
|
|
72
81
|
end
|
73
82
|
|
74
83
|
def all_workflows
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
find_workflow(id)
|
79
|
-
end
|
84
|
+
redis.scan_each(match: "gush.workflows.*").map do |key|
|
85
|
+
id = key.sub("gush.workflows.", "")
|
86
|
+
find_workflow(id)
|
80
87
|
end
|
81
88
|
end
|
82
89
|
|
83
90
|
def find_workflow(id)
|
84
|
-
|
85
|
-
data = redis.get("gush.workflows.#{id}")
|
91
|
+
data = redis.get("gush.workflows.#{id}")
|
86
92
|
|
87
|
-
|
88
|
-
|
89
|
-
|
93
|
+
unless data.nil?
|
94
|
+
hash = Gush::JSON.decode(data, symbolize_keys: true)
|
95
|
+
keys = redis.scan_each(match: "gush.jobs.#{id}.*")
|
90
96
|
|
91
|
-
|
92
|
-
|
93
|
-
end
|
94
|
-
|
95
|
-
workflow_from_hash(hash, nodes)
|
96
|
-
else
|
97
|
-
raise WorkflowNotFound.new("Workflow with given id doesn't exist")
|
97
|
+
nodes = keys.each_with_object([]) do |key, array|
|
98
|
+
array.concat redis.hvals(key).map { |json| Gush::JSON.decode(json, symbolize_keys: true) }
|
98
99
|
end
|
100
|
+
|
101
|
+
workflow_from_hash(hash, nodes)
|
102
|
+
else
|
103
|
+
raise WorkflowNotFound.new("Workflow with given id doesn't exist")
|
99
104
|
end
|
100
105
|
end
|
101
106
|
|
102
107
|
def persist_workflow(workflow)
|
103
|
-
|
104
|
-
redis.set("gush.workflows.#{workflow.id}", workflow.to_json)
|
105
|
-
end
|
108
|
+
redis.set("gush.workflows.#{workflow.id}", workflow.to_json)
|
106
109
|
|
107
110
|
workflow.jobs.each {|job| persist_job(workflow.id, job) }
|
108
111
|
workflow.mark_as_persisted
|
@@ -111,9 +114,7 @@ module Gush
|
|
111
114
|
end
|
112
115
|
|
113
116
|
def persist_job(workflow_id, job)
|
114
|
-
|
115
|
-
redis.hset("gush.jobs.#{workflow_id}.#{job.klass}", job.id, job.to_json)
|
116
|
-
end
|
117
|
+
redis.hset("gush.jobs.#{workflow_id}.#{job.klass}", job.id, job.to_json)
|
117
118
|
end
|
118
119
|
|
119
120
|
def find_job(workflow_id, job_name)
|
@@ -132,31 +133,23 @@ module Gush
|
|
132
133
|
end
|
133
134
|
|
134
135
|
def destroy_workflow(workflow)
|
135
|
-
|
136
|
-
redis.del("gush.workflows.#{workflow.id}")
|
137
|
-
end
|
136
|
+
redis.del("gush.workflows.#{workflow.id}")
|
138
137
|
workflow.jobs.each {|job| destroy_job(workflow.id, job) }
|
139
138
|
end
|
140
139
|
|
141
140
|
def destroy_job(workflow_id, job)
|
142
|
-
|
143
|
-
redis.del("gush.jobs.#{workflow_id}.#{job.klass}")
|
144
|
-
end
|
141
|
+
redis.del("gush.jobs.#{workflow_id}.#{job.klass}")
|
145
142
|
end
|
146
143
|
|
147
144
|
def expire_workflow(workflow, ttl=nil)
|
148
145
|
ttl = ttl || configuration.ttl
|
149
|
-
|
150
|
-
redis.expire("gush.workflows.#{workflow.id}", ttl)
|
151
|
-
end
|
146
|
+
redis.expire("gush.workflows.#{workflow.id}", ttl)
|
152
147
|
workflow.jobs.each {|job| expire_job(workflow.id, job, ttl) }
|
153
148
|
end
|
154
149
|
|
155
150
|
def expire_job(workflow_id, job, ttl=nil)
|
156
151
|
ttl = ttl || configuration.ttl
|
157
|
-
|
158
|
-
redis.expire("gush.jobs.#{workflow_id}.#{job.name}", ttl)
|
159
|
-
end
|
152
|
+
redis.expire("gush.jobs.#{workflow_id}.#{job.klass}", ttl)
|
160
153
|
end
|
161
154
|
|
162
155
|
def enqueue_job(workflow_id, job)
|
@@ -172,16 +165,11 @@ module Gush
|
|
172
165
|
def find_job_by_klass_and_id(workflow_id, job_name)
|
173
166
|
job_klass, job_id = job_name.split('|')
|
174
167
|
|
175
|
-
|
176
|
-
redis.hget("gush.jobs.#{workflow_id}.#{job_klass}", job_id)
|
177
|
-
end
|
168
|
+
redis.hget("gush.jobs.#{workflow_id}.#{job_klass}", job_id)
|
178
169
|
end
|
179
170
|
|
180
171
|
def find_job_by_klass(workflow_id, job_name)
|
181
|
-
new_cursor, result =
|
182
|
-
redis.hscan("gush.jobs.#{workflow_id}.#{job_name}", 0, count: 1)
|
183
|
-
end
|
184
|
-
|
172
|
+
new_cursor, result = redis.hscan("gush.jobs.#{workflow_id}.#{job_name}", 0, count: 1)
|
185
173
|
return nil if result.empty?
|
186
174
|
|
187
175
|
job_id, job = *result[0]
|
@@ -202,14 +190,8 @@ module Gush
|
|
202
190
|
flow
|
203
191
|
end
|
204
192
|
|
205
|
-
def
|
206
|
-
|
207
|
-
RedisClassy.redis = instance
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
def connection_pool
|
212
|
-
@connection_pool ||= ConnectionPool.new(size: configuration.concurrency, timeout: 1) { build_redis }
|
193
|
+
def redis
|
194
|
+
self.class.redis_connection(configuration)
|
213
195
|
end
|
214
196
|
end
|
215
197
|
end
|
data/lib/gush/worker.rb
CHANGED
@@ -6,6 +6,12 @@ module Gush
|
|
6
6
|
def perform(workflow_id, job_id)
|
7
7
|
setup_job(workflow_id, job_id)
|
8
8
|
|
9
|
+
if job.succeeded?
|
10
|
+
# Try to enqueue outgoing jobs again because the last job has redis mutex lock error
|
11
|
+
enqueue_outgoing_jobs
|
12
|
+
return
|
13
|
+
end
|
14
|
+
|
9
15
|
job.payloads = incoming_payloads
|
10
16
|
|
11
17
|
error = nil
|
@@ -75,6 +81,8 @@ module Gush
|
|
75
81
|
end
|
76
82
|
end
|
77
83
|
end
|
84
|
+
rescue RedisMutex::LockError
|
85
|
+
Worker.set(wait: 2.seconds).perform_later(workflow_id, job.name)
|
78
86
|
end
|
79
87
|
end
|
80
88
|
end
|
data/spec/gush/client_spec.rb
CHANGED
@@ -95,12 +95,18 @@ describe Gush::Client do
|
|
95
95
|
end
|
96
96
|
|
97
97
|
describe "#expire_workflow" do
|
98
|
+
let(:ttl) { 2000 }
|
99
|
+
|
98
100
|
it "sets TTL for all Redis keys related to the workflow" do
|
99
101
|
workflow = TestWorkflow.create
|
100
102
|
|
101
|
-
client.expire_workflow(workflow,
|
103
|
+
client.expire_workflow(workflow, ttl)
|
104
|
+
|
105
|
+
expect(redis.ttl("gush.workflows.#{workflow.id}")).to eq(ttl)
|
102
106
|
|
103
|
-
|
107
|
+
workflow.jobs.each do |job|
|
108
|
+
expect(redis.ttl("gush.jobs.#{workflow.id}.#{job.klass}")).to eq(ttl)
|
109
|
+
end
|
104
110
|
end
|
105
111
|
end
|
106
112
|
|
data/spec/gush/worker_spec.rb
CHANGED
@@ -39,6 +39,18 @@ describe Gush::Worker do
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
+
context 'when job failed to enqueue outgoing jobs' do
|
43
|
+
it 'enqeues another job to handling enqueue_outgoing_jobs' do
|
44
|
+
allow(RedisMutex).to receive(:with_lock).and_raise(RedisMutex::LockError)
|
45
|
+
subject.perform(workflow.id, 'Prepare')
|
46
|
+
expect(Gush::Worker).to have_no_jobs(workflow.id, jobs_with_id(["FetchFirstJob", "FetchSecondJob"]))
|
47
|
+
|
48
|
+
allow(RedisMutex).to receive(:with_lock).and_call_original
|
49
|
+
perform_one
|
50
|
+
expect(Gush::Worker).to have_jobs(workflow.id, jobs_with_id(["FetchFirstJob", "FetchSecondJob"]))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
42
54
|
it "calls job.perform method" do
|
43
55
|
SPY = double()
|
44
56
|
expect(SPY).to receive(:some_method)
|
data/spec/spec_helper.rb
CHANGED
@@ -78,6 +78,19 @@ RSpec::Matchers.define :have_jobs do |flow, jobs|
|
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
81
|
+
RSpec::Matchers.define :have_no_jobs do |flow, jobs|
|
82
|
+
match do |actual|
|
83
|
+
expected = jobs.map do |job|
|
84
|
+
hash_including(args: include(flow, job))
|
85
|
+
end
|
86
|
+
expect(ActiveJob::Base.queue_adapter.enqueued_jobs).not_to match_array(expected)
|
87
|
+
end
|
88
|
+
|
89
|
+
failure_message do |actual|
|
90
|
+
"expected queue to have no #{jobs}, but instead has: #{ActiveJob::Base.queue_adapter.enqueued_jobs.map{ |j| j[:args][1]}}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
81
94
|
RSpec.configure do |config|
|
82
95
|
config.include ActiveJob::TestHelper
|
83
96
|
config.include GushHelpers
|
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: 2.0.
|
4
|
+
version: 2.0.2
|
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:
|
11
|
+
date: 2022-03-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -19,7 +19,7 @@ dependencies:
|
|
19
19
|
version: 4.2.7
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: '
|
22
|
+
version: '7.0'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -29,21 +29,21 @@ dependencies:
|
|
29
29
|
version: 4.2.7
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '
|
32
|
+
version: '7.0'
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
|
-
name:
|
34
|
+
name: concurrent-ruby
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version:
|
39
|
+
version: '1.0'
|
40
40
|
type: :runtime
|
41
41
|
prerelease: false
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
43
43
|
requirements:
|
44
44
|
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version:
|
46
|
+
version: '1.0'
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: multi_json
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
@@ -180,16 +180,16 @@ dependencies:
|
|
180
180
|
name: bundler
|
181
181
|
requirement: !ruby/object:Gem::Requirement
|
182
182
|
requirements:
|
183
|
-
- - "
|
183
|
+
- - ">="
|
184
184
|
- !ruby/object:Gem::Version
|
185
|
-
version: '
|
185
|
+
version: '0'
|
186
186
|
type: :development
|
187
187
|
prerelease: false
|
188
188
|
version_requirements: !ruby/object:Gem::Requirement
|
189
189
|
requirements:
|
190
|
-
- - "
|
190
|
+
- - ">="
|
191
191
|
- !ruby/object:Gem::Version
|
192
|
-
version: '
|
192
|
+
version: '0'
|
193
193
|
- !ruby/object:Gem::Dependency
|
194
194
|
name: rake
|
195
195
|
requirement: !ruby/object:Gem::Requirement
|
@@ -307,8 +307,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
307
307
|
- !ruby/object:Gem::Version
|
308
308
|
version: '0'
|
309
309
|
requirements: []
|
310
|
-
|
311
|
-
rubygems_version: 2.7.6
|
310
|
+
rubygems_version: 3.1.4
|
312
311
|
signing_key:
|
313
312
|
specification_version: 4
|
314
313
|
summary: Fast and distributed workflow runner based on ActiveJob and Redis
|