dwf 0.1.10 → 0.1.11

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: fba0bf3a5195787db6540ddfe2431a265b09b78859f333ed9295b4e4677d0a25
4
- data.tar.gz: 66488a6107dec45933fc1631e713db3fb43a6e31dbd5d5a351a79a6bbb31f1f7
3
+ metadata.gz: e545ff4c6f4b56071875f642f1262fee7762fabb63d86c816a0c943cb8e34657
4
+ data.tar.gz: dfe832f98724b81b8ff1198320cb66013869cca6ba2b2f89d47c4caf0b2108f9
5
5
  SHA512:
6
- metadata.gz: 88c01767c12ae11ff5f2aaeaf68b08a8c04a54bb909c51babd0f02cd915b1c8d790131c197034bbce46a8ab4d8e0dfa40a0f7afe4f1353bff65d952654438454
7
- data.tar.gz: 799827548d718f4ae763c00c4519f636f448eb0aba40e6d5a60f83443cf2f479b3d8fbfd536744e11fc9092d22be3a18a7bb764b25cafcb27eccef3168ab0170
6
+ metadata.gz: 7e77ca9abe614aa1da026818f1a49028bcc3878bd9bd5838e8cdfdf245ab09d124ab4300bd077eed1c3e8b07e46b321c1dd59d18face658fda3f6c636e28954f
7
+ data.tar.gz: 6a6ff8756a13fcec73966c41ded7b808cae24a73e1cec2bd301dacfdbfde6022812c7f27c6dacc12a5bb469aae241e59316eb544b2ca7ac1f0530e1c5f5ffa2c
data/.gitignore CHANGED
@@ -4,3 +4,4 @@ Gemfile.lock
4
4
  dwf-*.gem
5
5
  :w
6
6
  :W
7
+ coverage/
data/CHANGELOG.md CHANGED
@@ -1,5 +1,72 @@
1
1
  # Changelog
2
2
  All notable changes to this project will be documented in this file.
3
+ ## 0.1.11
4
+ ### Added
5
+ #### Subworkflow - Only support sidekiq pro
6
+ There might be a case when you want to reuse a workflow in another workflow
7
+
8
+ As an example, let's write a workflow which contain another workflow, expected that the SubWorkflow workflow execute after `SecondItem` and the `ThirdItem` execute after `SubWorkflow`
9
+
10
+ ```ruby
11
+ gem 'dwf', '~> 0.1.11'
12
+ ```
13
+
14
+ ### Setup
15
+ ```ruby
16
+ class FirstItem < Dwf::Item
17
+ def perform
18
+ puts "Main flow: #{self.class.name} running"
19
+ puts "Main flow: #{self.class.name} finish"
20
+ end
21
+ end
22
+
23
+ SecondItem = Class.new(FirstItem)
24
+ ThirtItem = Class.new(FirstItem)
25
+
26
+ class FirstSubItem < Dwf::Item
27
+ def perform
28
+ puts "Sub flow: #{self.class.name} running"
29
+ puts "Sub flow: #{self.class.name} finish"
30
+ end
31
+ end
32
+
33
+ SecondSubItem = Class.new(FirstSubItem)
34
+
35
+ class SubWorkflow < Dwf::Workflow
36
+ def configure
37
+ run FirstSubItem
38
+ run SecondSubItem, after: FirstSubItem
39
+ end
40
+ end
41
+
42
+
43
+ class TestWf < Dwf::Workflow
44
+ def configure
45
+ run FirstItem
46
+ run SecondItem, after: FirstItem
47
+ run SubWorkflow, after: SecondItem
48
+ run ThirtItem, after: SubWorkflow
49
+ end
50
+ end
51
+
52
+ wf = TestWf.create
53
+ wf.start!
54
+ ```
55
+
56
+ ### Result
57
+ ```
58
+ Main flow: FirstItem running
59
+ Main flow: FirstItem finish
60
+ Main flow: SecondItem running
61
+ Main flow: SecondItem finish
62
+ Sub flow: FirstSubItem running
63
+ Sub flow: FirstSubItem finish
64
+ Sub flow: SecondSubItem running
65
+ Sub flow: SecondSubItem finish
66
+ Main flow: ThirtItem running
67
+ Main flow: ThirtItem finish
68
+ ```
69
+
3
70
  ## 0.1.10
4
71
  ### Added
5
72
  - Allow to use argument within workflow and update the defining callback way
data/README.md CHANGED
@@ -4,22 +4,45 @@
4
4
  # Installation
5
5
  ## 1. Add `dwf` to Gemfile
6
6
  ```ruby
7
- gem 'dwf', '~> 0.1.9'
7
+ gem 'dwf', '~> 0.1.10'
8
8
  ```
9
- ## 2. Execute flow
9
+ ## 2. Execute flow example
10
10
  ### Declare jobs
11
11
 
12
12
  ```ruby
13
13
  require 'dwf'
14
14
 
15
- class A < Dwf::Item
15
+ class FirstItem < Dwf::Item
16
16
  def perform
17
- puts "#{self.class.name} Working"
18
- sleep 2
19
- puts params
20
- puts "#{self.class.name} Finished"
17
+ puts "#{self.class.name}: running"
18
+ puts "#{self.class.name}: finish"
21
19
  end
22
20
  end
21
+
22
+ class SecondItem < Dwf::Item
23
+ def perform
24
+ puts "#{self.class.name}: running"
25
+ output('Send to ThirdItem')
26
+ puts "#{self.class.name} finish"
27
+ end
28
+ end
29
+
30
+ class ThirdItem < Dwf::Item
31
+ def perform
32
+ puts "#{self.class.name}: running"
33
+ puts "#{self.class.name}: finish"
34
+ end
35
+ end
36
+
37
+ class FourthItem < Dwf::Item
38
+ def perform
39
+ puts "#{self.class.name}: running"
40
+ puts "payloads from incoming: #{payloads.inspect}"
41
+ puts "#{self.class.name}: finish"
42
+ end
43
+ end
44
+
45
+ FifthItem = Class.new(FirstItem)
23
46
  ```
24
47
 
25
48
  ### Declare flow
@@ -28,16 +51,18 @@ require 'dwf'
28
51
 
29
52
  class TestWf < Dwf::Workflow
30
53
  def configure
31
- run A
32
- run B, after: A
33
- run C, after: A
34
- run E, after: [B, C], params: 'E say hello'
35
- run D, after: [E], params: 'D say hello'
36
- run F, params: 'F say hello'
54
+ run FirstItem
55
+ run SecondItem, after: FirstItem
56
+ run ThirdItem, after: FirstItem
57
+ run FourthItem, after: [ThirdItem, SecondItem]
58
+ run FifthItem, after: FourthItem
37
59
  end
38
60
  end
39
61
  ```
40
-
62
+ ### Start background worker process
63
+ ```
64
+ bundle exec sidekiq -q dwf
65
+ ```
41
66
 
42
67
  ### Execute flow
43
68
  ```ruby
@@ -55,21 +80,16 @@ By default `dwf` will use `Dwf::Workflow::BUILD_IN` callback.
55
80
 
56
81
  ### Output
57
82
  ```
58
- A Working
59
- F Working
60
- A Finished
61
- F say hello
62
- F Finished
63
- C Working
64
- B Working
65
- C Finished
66
- B Finished
67
- E Working
68
- E say hello
69
- E Finished
70
- D Working
71
- D say hello
72
- D Finished
83
+ FirstItem: running
84
+ FirstItem: finish
85
+ SecondItem: running
86
+ SecondItem finish
87
+ ThirdItem: running
88
+ ThirdItem: finish
89
+ FourthItem: running
90
+ FourthItem: finish
91
+ FifthItem: running
92
+ FifthItem: finish
73
93
  ```
74
94
 
75
95
  # Config redis and default queue
@@ -84,8 +104,8 @@ Dwf.config do |config|
84
104
  config.namespace = 'dwf'
85
105
  end
86
106
  ```
87
-
88
- # Pinelining
107
+ # Advanced features
108
+ ## Pipelining
89
109
  You can pass jobs result to next nodes
90
110
 
91
111
  ```ruby
@@ -118,6 +138,70 @@ end
118
138
  }
119
139
  ]
120
140
  ```
141
+ ## Subworkflow - Only support sidekiq pro
142
+ There might be a case when you want to reuse a workflow in another workflow
143
+
144
+ As an example, let's write a workflow which contain another workflow, expected that the SubWorkflow workflow execute after `SecondItem` and the `ThirdItem` execute after `SubWorkflow`
145
+
146
+ ```ruby
147
+ gem 'dwf', '~> 0.1.11'
148
+ ```
149
+
150
+ ### Setup
151
+ ```ruby
152
+ class FirstItem < Dwf::Item
153
+ def perform
154
+ puts "Main flow: #{self.class.name} running"
155
+ puts "Main flow: #{self.class.name} finish"
156
+ end
157
+ end
158
+
159
+ SecondItem = Class.new(FirstItem)
160
+ ThirtItem = Class.new(FirstItem)
161
+
162
+ class FirstSubItem < Dwf::Item
163
+ def perform
164
+ puts "Sub flow: #{self.class.name} running"
165
+ puts "Sub flow: #{self.class.name} finish"
166
+ end
167
+ end
168
+
169
+ SecondSubItem = Class.new(FirstSubItem)
170
+
171
+ class SubWorkflow < Dwf::Workflow
172
+ def configure
173
+ run FirstSubItem
174
+ run SecondSubItem, after: FirstSubItem
175
+ end
176
+ end
177
+
178
+
179
+ class TestWf < Dwf::Workflow
180
+ def configure
181
+ run FirstItem
182
+ run SecondItem, after: FirstItem
183
+ run SubWorkflow, after: SecondItem
184
+ run ThirtItem, after: SubWorkflow
185
+ end
186
+ end
187
+
188
+ wf = TestWf.create
189
+ wf.start!
190
+ ```
191
+
192
+ ### Result
193
+ ```
194
+ Main flow: FirstItem running
195
+ Main flow: FirstItem finish
196
+ Main flow: SecondItem running
197
+ Main flow: SecondItem finish
198
+ Sub flow: FirstSubItem running
199
+ Sub flow: FirstSubItem finish
200
+ Sub flow: SecondSubItem running
201
+ Sub flow: SecondSubItem finish
202
+ Main flow: ThirtItem running
203
+ Main flow: ThirtItem finish
204
+ ```
121
205
 
122
206
  # Todo
123
207
  - [x] Make it work
@@ -125,9 +209,9 @@ end
125
209
  - [x] Support with build-in callback
126
210
  - [x] Add github workflow
127
211
  - [x] Redis configurable
128
- - [x] Pinelining
212
+ - [x] Pipelining
129
213
  - [X] Test
130
- - [ ] Consistent item name
214
+ - [ ] WIP - subworkflow
131
215
  - [ ] Support [Resque](https://github.com/resque/resque)
132
216
  - [ ] Key value store plugable
133
217
  - [ ] research https://github.com/moneta-rb/moneta
data/dwf.gemspec CHANGED
@@ -3,10 +3,11 @@
3
3
 
4
4
  lib = File.expand_path('../lib', __FILE__)
5
5
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require_relative 'lib/dwf/version'
6
7
 
7
8
  Gem::Specification.new do |spec|
8
9
  spec.name = "dwf"
9
- spec.version = '0.1.10'
10
+ spec.version = Dwf::VERSION
10
11
  spec.authors = ["dthtien"]
11
12
  spec.email = ["tiendt2311@gmail.com"]
12
13
 
@@ -25,8 +26,9 @@ Gem::Specification.new do |spec|
25
26
  # guide at: https://bundler.io/guides/creating_gem.html
26
27
 
27
28
  spec.add_development_dependency 'byebug', '~> 11.1.3'
29
+ spec.add_development_dependency 'mock_redis', '~> 0.27.2'
28
30
  spec.add_dependency 'redis', '~> 4.2.0'
29
31
  spec.add_development_dependency 'rspec', '~> 3.2'
30
- spec.add_development_dependency 'mock_redis', '~> 0.27.2'
31
32
  spec.add_dependency 'sidekiq', '~> 6.2.0'
33
+ spec.add_development_dependency 'simplecov'
32
34
  end
data/lib/dwf/callback.rb CHANGED
@@ -9,8 +9,8 @@ module Dwf
9
9
  previous_job_names = options['names']
10
10
  workflow_id = options['workflow_id']
11
11
  processing_job_names = previous_job_names.map do |job_name|
12
- job = client.find_job(workflow_id, job_name)
13
- job.outgoing
12
+ node = client.find_node(job_name, workflow_id)
13
+ node.outgoing
14
14
  end.flatten.uniq
15
15
  return if processing_job_names.empty?
16
16
 
@@ -19,7 +19,7 @@ module Dwf
19
19
  end
20
20
 
21
21
  def start(job)
22
- job.outgoing.any? ? start_with_batch(job) : job.perform_async
22
+ job.outgoing.any? ? start_with_batch(job) : job.persist_and_perform_async!
23
23
  end
24
24
 
25
25
  private
@@ -40,11 +40,13 @@ module Dwf
40
40
  batch.on(
41
41
  :success,
42
42
  'Dwf::Callback#process_next_step',
43
- names: jobs.map(&:klass),
43
+ names: jobs.map(&:name),
44
44
  workflow_id: workflow_id
45
45
  )
46
46
  batch.jobs do
47
- jobs.each { |job| job.persist_and_perform_async! if job.ready_to_start? }
47
+ jobs.each do |job|
48
+ job.persist_and_perform_async! if job.ready_to_start?
49
+ end
48
50
  end
49
51
  end
50
52
 
@@ -61,7 +63,7 @@ module Dwf
61
63
 
62
64
  def fetch_jobs(processing_job_names, workflow_id)
63
65
  processing_job_names.map do |job_name|
64
- client.find_job(workflow_id, job_name)
66
+ client.find_node(job_name, workflow_id)
65
67
  end.compact
66
68
  end
67
69
 
@@ -71,15 +73,16 @@ module Dwf
71
73
  client.release_lock(workflow_id, job_name)
72
74
  end
73
75
 
74
- def start_with_batch(job)
76
+ def start_with_batch(node)
75
77
  batch = Sidekiq::Batch.new
78
+ workflow_id = node.is_a?(Dwf::Workflow) ? node.parent_id : node.workflow_id
76
79
  batch.on(
77
80
  :success,
78
81
  'Dwf::Callback#process_next_step',
79
- names: [job.name],
80
- workflow_id: job.workflow_id
82
+ names: [node.name],
83
+ workflow_id: workflow_id
81
84
  )
82
- batch.jobs { job.perform_async }
85
+ batch.jobs { node.persist_and_perform_async! }
83
86
  end
84
87
 
85
88
  def client
data/lib/dwf/client.rb CHANGED
@@ -22,8 +22,22 @@ module Dwf
22
22
  Dwf::Item.from_hash(Dwf::Utils.symbolize_keys(data))
23
23
  end
24
24
 
25
+ def find_node(name, workflow_id)
26
+ if Utils.workflow_name?(name)
27
+ if name.include?('|')
28
+ _, id = name.split('|')
29
+ else
30
+ id = workflow_id(name, workflow_id)
31
+ end
32
+ find_workflow(id)
33
+ else
34
+ find_job(workflow_id, name)
35
+ end
36
+ end
37
+
25
38
  def find_workflow(id)
26
- data = redis.get("dwf.workflows.#{id}")
39
+ key = redis.keys("dwf.workflows.#{id}*").first
40
+ data = redis.get(key)
27
41
  raise WorkflowNotFound, "Workflow with given id doesn't exist" if data.nil?
28
42
 
29
43
  hash = JSON.parse(data)
@@ -32,6 +46,19 @@ module Dwf
32
46
  workflow_from_hash(hash, nodes)
33
47
  end
34
48
 
49
+ def find_sub_workflow(name, parent_id)
50
+ find_workflow(workflow_id(name, parent_id))
51
+ end
52
+
53
+ def sub_workflows(id)
54
+ keys = redis.keys("dwf.workflows.*.*.#{id}")
55
+ keys.map do |key|
56
+ id = key.split('.')[2]
57
+
58
+ find_workflow(id)
59
+ end
60
+ end
61
+
35
62
  def persist_job(job)
36
63
  redis.hset("dwf.jobs.#{job.workflow_id}.#{job.klass}", job.id, job.as_json)
37
64
  end
@@ -51,7 +78,10 @@ module Dwf
51
78
  end
52
79
 
53
80
  def persist_workflow(workflow)
54
- redis.set("dwf.workflows.#{workflow.id}", workflow.as_json)
81
+ key = [
82
+ 'dwf', 'workflows', workflow.id, workflow.class.name, workflow.parent_id
83
+ ].compact.join('.')
84
+ redis.set(key, workflow.as_json)
55
85
  end
56
86
 
57
87
  def build_job_id(workflow_id, job_klass)
@@ -96,6 +126,13 @@ module Dwf
96
126
 
97
127
  private
98
128
 
129
+ def workflow_id(name, parent_id)
130
+ key = redis.keys("dwf.workflows.*.#{name}.#{parent_id}").first
131
+ return if key.nil?
132
+
133
+ key.split('.')[2]
134
+ end
135
+
99
136
  def find_job_by_klass_and_id(workflow_id, job_name)
100
137
  job_klass, job_id = job_name.split('|')
101
138
 
@@ -114,22 +151,26 @@ module Dwf
114
151
  def parse_nodes(id)
115
152
  keys = redis.scan_each(match: "dwf.jobs.#{id}.*")
116
153
 
117
- keys.map do |key|
154
+ items = keys.map do |key|
118
155
  redis.hvals(key).map do |json|
119
- Dwf::Utils.symbolize_keys JSON.parse(json)
156
+ node = Dwf::Utils.symbolize_keys JSON.parse(json)
157
+ Dwf::Item.from_hash(node)
120
158
  end
121
159
  end.flatten
160
+ workflows = sub_workflows(id)
161
+ items + workflows
122
162
  end
123
163
 
124
- def workflow_from_hash(hash, nodes = [])
164
+ def workflow_from_hash(hash, jobs = [])
125
165
  flow = Module.const_get(hash[:klass]).new(*hash[:arguments])
126
166
  flow.jobs = []
167
+ flow.outgoing = hash.fetch(:outgoing, [])
168
+ flow.parent_id = hash[:parent_id]
169
+ flow.incoming = hash.fetch(:incoming, [])
127
170
  flow.stopped = hash.fetch(:stopped, false)
171
+ flow.callback_type = hash.fetch(:callback_type, Workflow::BUILD_IN)
128
172
  flow.id = hash[:id]
129
- flow.jobs = nodes.map do |node|
130
- Dwf::Item.from_hash(node)
131
- end
132
-
173
+ flow.jobs = jobs
133
174
  flow
134
175
  end
135
176
 
@@ -0,0 +1,29 @@
1
+ module Dwf
2
+ module Concerns
3
+ module Checkable
4
+ def no_dependencies?
5
+ incoming.empty?
6
+ end
7
+
8
+ def leaf?
9
+ outgoing.empty?
10
+ end
11
+
12
+ def ready_to_start?
13
+ !running? && !enqueued? && !finished? && !failed? && parents_succeeded?
14
+ end
15
+
16
+ def succeeded?
17
+ finished? && !failed?
18
+ end
19
+
20
+ def running?
21
+ started? && !finished?
22
+ end
23
+
24
+ def started?
25
+ !!started_at
26
+ end
27
+ end
28
+ end
29
+ end
data/lib/dwf/errors.rb CHANGED
@@ -1,3 +1,5 @@
1
1
  module Dwf
2
- class WorkflowNotFound < StandardError; end
2
+ class WorkflowNotFound < StandardError; end
3
+
4
+ class UnsupportCallback < StandardError; end
3
5
  end
data/lib/dwf/item.rb CHANGED
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'client'
4
+ require_relative 'concerns/checkable'
4
5
 
5
6
  module Dwf
6
7
  class Item
8
+ include Concerns::Checkable
9
+
7
10
  attr_reader :workflow_id, :id, :params, :queue, :klass, :started_at,
8
- :enqueued_at, :finished_at, :failed_at, :callback_type, :output_payload
9
- attr_accessor :incoming, :outgoing
11
+ :enqueued_at, :finished_at, :failed_at, :output_payload
12
+ attr_writer :payloads
13
+ attr_accessor :incoming, :outgoing, :callback_type
10
14
 
11
15
  def initialize(options = {})
12
16
  assign_attributes(options)
@@ -16,9 +20,17 @@ module Dwf
16
20
  Module.const_get(hash[:klass]).new(hash)
17
21
  end
18
22
 
23
+ def start_initial!
24
+ cb_build_in? ? persist_and_perform_async! : start_batch!
25
+ end
26
+
27
+ def start_batch!
28
+ enqueue_and_persist!
29
+ Dwf::Callback.new.start(self)
30
+ end
31
+
19
32
  def persist_and_perform_async!
20
- enqueue!
21
- persist!
33
+ enqueue_and_persist!
22
34
  perform_async
23
35
  end
24
36
 
@@ -35,7 +47,7 @@ module Dwf
35
47
 
36
48
  def perform_async
37
49
  Dwf::Worker.set(queue: queue || client.config.namespace)
38
- .perform_async(workflow_id, name)
50
+ .perform_async(workflow_id, name)
39
51
  end
40
52
 
41
53
  def name
@@ -51,20 +63,11 @@ module Dwf
51
63
  end
52
64
 
53
65
  def parents_succeeded?
54
- incoming.all? do |name|
55
- client.find_job(workflow_id, name).succeeded?
56
- end
66
+ incoming.all? { |name| client.find_node(name, workflow_id).succeeded? }
57
67
  end
58
68
 
59
69
  def payloads
60
- incoming.map do |job_name|
61
- job = client.find_job(workflow_id, job_name)
62
- {
63
- id: job.name,
64
- class: job.klass.to_s,
65
- output: job.output_payload
66
- }
67
- end
70
+ @payloads ||= build_payloads
68
71
  end
69
72
 
70
73
  def enqueue!
@@ -109,22 +112,6 @@ module Dwf
109
112
  !failed_at.nil?
110
113
  end
111
114
 
112
- def succeeded?
113
- finished? && !failed?
114
- end
115
-
116
- def started?
117
- !started_at.nil?
118
- end
119
-
120
- def running?
121
- started? && !finished?
122
- end
123
-
124
- def ready_to_start?
125
- !running? && !enqueued? && !finished? && !failed? && parents_succeeded?
126
- end
127
-
128
115
  def current_timestamp
129
116
  Time.now.to_i
130
117
  end
@@ -152,7 +139,8 @@ module Dwf
152
139
  params: params,
153
140
  workflow_id: workflow_id,
154
141
  callback_type: callback_type,
155
- output_payload: output_payload
142
+ output_payload: output_payload,
143
+ payloads: @payloads
156
144
  }
157
145
  end
158
146
 
@@ -166,6 +154,11 @@ module Dwf
166
154
 
167
155
  private
168
156
 
157
+ def enqueue_and_persist!
158
+ enqueue!
159
+ persist!
160
+ end
161
+
169
162
  def client
170
163
  @client ||= Dwf::Client.new
171
164
  end
@@ -184,6 +177,21 @@ module Dwf
184
177
  @started_at = options[:started_at]
185
178
  @callback_type = options[:callback_type]
186
179
  @output_payload = options[:output_payload]
180
+ @payloads = options[:payloads]
181
+ end
182
+
183
+ def build_payloads
184
+ data = incoming.map do |job_name|
185
+ node = client.find_node(job_name, workflow_id)
186
+ next if node.output_payload.nil?
187
+
188
+ {
189
+ id: node.name,
190
+ class: node.klass.to_s,
191
+ output: node.output_payload
192
+ }
193
+ end.compact
194
+ data.empty? ? nil : data
187
195
  end
188
196
  end
189
197
  end
data/lib/dwf/utils.rb CHANGED
@@ -34,5 +34,11 @@ module Dwf
34
34
  obj
35
35
  end
36
36
  end
37
+
38
+ def self.workflow_name?(name)
39
+ node_name = name.include?('|') ? name.split('|').first : name
40
+
41
+ Module.const_get(node_name) <= Workflow
42
+ end
37
43
  end
38
44
  end
data/lib/dwf/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dwf
4
- VERSION = '0.1.8'
4
+ VERSION = '0.1.11'
5
5
  end