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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9cfd59c5cadd225e30c41bcd7a5293893d78041056409c2e1fd15316d2cc4d75
4
- data.tar.gz: ef3e28f72d5ed90bb175f9452e189111fbc5bb95facc9585df1c660fd691038f
3
+ metadata.gz: a9e61295e87f5c707a6f93a3cce7238c0650876be7ff76c4cba0c60133832217
4
+ data.tar.gz: 7df67c0134bc1e7bd036da850287191463557a764279790072f3f0a3887bf44b
5
5
  SHA512:
6
- metadata.gz: 38578ba5a3f159577d38d6af662137fc5c9b95a394c01f498b22d6ff2931fc3e78eb9634f9c0ae1da683d197412f0ee38c43efa6035d223f1ece1d09b483cb9d
7
- data.tar.gz: bf8954ea6d409a1fc9db19e24c4dae322f15cf2d3e9240df17d2ff847503318fbdcf0f50b27c08910273fb6dfc3223919f79679d1986670b0483b2b27f627384
6
+ metadata.gz: 133913d52aaef1d470ca8be41c865c26949577df06935b034e83bb0ee616f5cd812d0c49730bbfc504342a46a6f0c6b813df19705f0a6c4448ba18969172e418
7
+ data.tar.gz: b08039f50aba1ab24fbe957669f16d51258913353ffba98b168a9df74782b46459c79f083c7e54b1dfa717a5878e85bb72ba07397993f833e83f9733ec5089dd
@@ -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
@@ -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 = "1.1.1"
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"
@@ -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
- job_identifier = nil
45
+ def next_free_job_id(workflow_id, job_klass)
46
+ job_id = nil
47
+
47
48
  loop do
48
- id = SecureRandom.uuid
49
- job_identifier = "#{job_klass}-#{id}"
49
+ job_id = SecureRandom.uuid
50
50
  available = connection_pool.with do |redis|
51
- !redis.exists("gush.jobs.#{workflow_id}.#{job_identifier}")
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
- job_identifier
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
- nodes = redis.mget(*keys).map { |json| Gush::JSON.decode(json, symbolize_keys: true) }
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.set("gush.jobs.#{workflow_id}.#{job.name}", job.to_json)
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, job_id)
115
- job_name_match = /(?<klass>\w*[^-])-(?<identifier>.*)/.match(job_id)
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 = connection_pool.with do |redis|
125
- redis.get(keys.first)
126
- end
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.name}")
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: configuration.namespace).perform_later(*[workflow_id, job.name])
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
@@ -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 :name, :output_payload, :params
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
- name: name,
15
- klass: self.class.to_s,
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
- @name = opts[:name]
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
@@ -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
- out = client.find_job(workflow_id, job_name)
70
- if out.ready_to_start?
71
- client.enqueue_job(workflow_id, out)
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
@@ -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.class.to_s == name.to_s }
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
- name: client.next_free_job_id(id, klass.to_s),
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
@@ -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, name: "a-job", finished_at: 123, enqueued_at: 120)
65
+ job = described_class.new(workflow_id: 123, id: "702bced5-bb72-4bba-8f6f-15a3afa358bd", finished_at: 123, enqueued_at: 120)
66
66
  expected = {
67
- name: "a-job",
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
- name: 'gob',
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.name).to eq('gob')
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)
@@ -135,6 +135,7 @@ describe Gush::Workflow do
135
135
  klass1 = Class.new(Gush::Job)
136
136
  klass2 = Class.new(Gush::Job)
137
137
  klass3 = Class.new(Gush::Job)
138
+
138
139
  tree.run(klass1)
139
140
  tree.run(klass2, after: [klass1, klass3])
140
141
  tree.run(klass3)
@@ -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}-(?<identifier>.*)/
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: 1.1.1
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-06-09 00:00:00.000000000 Z
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