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 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