dwf 0.1.10 → 0.1.11

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