dwf 0.1.10 → 0.1.11

Sign up to get free protection for your applications and to get access to all the features.
data/lib/dwf/workflow.rb CHANGED
@@ -2,16 +2,20 @@
2
2
 
3
3
  require_relative 'client'
4
4
  require_relative 'worker'
5
+ require_relative 'concerns/checkable'
5
6
  require_relative 'callback'
6
7
 
7
8
  module Dwf
8
9
  class Workflow
10
+ include Concerns::Checkable
11
+
9
12
  CALLBACK_TYPES = [
10
13
  BUILD_IN = 'build-in',
11
14
  SK_BATCH = 'sk-batch'
12
15
  ].freeze
13
- attr_accessor :jobs, :callback_type, :stopped, :id
14
- attr_reader :dependencies, :started_at, :finished_at, :persisted, :arguments
16
+ attr_accessor :jobs, :stopped, :id, :incoming, :outgoing, :parent_id
17
+ attr_reader :dependencies, :started_at, :finished_at, :persisted, :arguments, :klass,
18
+ :callback_type
15
19
 
16
20
  class << self
17
21
  def create(*args)
@@ -31,8 +35,12 @@ module Dwf
31
35
  @jobs = []
32
36
  @persisted = false
33
37
  @stopped = false
34
- @arguments = args
38
+ @arguments = *args
39
+ @parent_id = nil
40
+ @klass = self.class
35
41
  @callback_type = BUILD_IN
42
+ @incoming = []
43
+ @outgoing = []
36
44
 
37
45
  setup
38
46
  end
@@ -44,16 +52,42 @@ module Dwf
44
52
  true
45
53
  end
46
54
 
55
+ def name
56
+ "#{self.class.name}|#{id}"
57
+ end
58
+
59
+ def sub_workflow?
60
+ !parent_id.nil?
61
+ end
62
+
63
+ def callback_type=(type = BUILD_IN)
64
+ @callback_type = type
65
+ jobs.each { |job| job.callback_type = type }
66
+ end
67
+
47
68
  alias save persist!
48
69
 
49
70
  def start!
71
+ raise UnsupportCallback, 'Sub workflow only works with Sidekiq batch callback' if invalid_callback?
72
+
50
73
  mark_as_started
51
74
  persist!
52
75
  initial_jobs.each do |job|
53
- cb_build_in? ? job.persist_and_perform_async! : Dwf::Callback.new.start(job)
76
+ job.payloads = payloads if sub_workflow?
77
+ job.start_initial!
54
78
  end
55
79
  end
56
80
 
81
+ def payloads
82
+ @payloads ||= build_payloads
83
+ end
84
+
85
+ def start_initial!
86
+ cb_build_in? ? start! : Callback.new.start(self)
87
+ end
88
+
89
+ alias persist_and_perform_async! start!
90
+
57
91
  def reload
58
92
  flow = self.class.find(id)
59
93
  self.stopped = flow.stopped
@@ -73,14 +107,7 @@ module Dwf
73
107
  def configure(*arguments); end
74
108
 
75
109
  def run(klass, options = {})
76
- node = klass.new(
77
- workflow_id: id,
78
- id: client.build_job_id(id, klass.to_s),
79
- params: options.fetch(:params, {}),
80
- queue: options[:queue],
81
- callback_type: callback_type
82
- )
83
-
110
+ node = build_node(klass, options)
84
111
  jobs << node
85
112
 
86
113
  build_dependencies_structure(node, options)
@@ -112,7 +139,10 @@ module Dwf
112
139
  stopped: stopped,
113
140
  started_at: started_at,
114
141
  finished_at: finished_at,
115
- callback_type: callback_type
142
+ callback_type: callback_type,
143
+ incoming: incoming,
144
+ outgoing: outgoing,
145
+ parent_id: parent_id
116
146
  }
117
147
  end
118
148
 
@@ -124,13 +154,7 @@ module Dwf
124
154
  jobs.all?(&:finished?)
125
155
  end
126
156
 
127
- def started?
128
- !!started_at
129
- end
130
-
131
- def running?
132
- started? && !finished?
133
- end
157
+ alias enqueued? started?
134
158
 
135
159
  def failed?
136
160
  jobs.any?(&:failed?)
@@ -140,6 +164,10 @@ module Dwf
140
164
  stopped
141
165
  end
142
166
 
167
+ def parents_succeeded?
168
+ incoming.all? { |name| client.find_node(name, parent_id).succeeded? }
169
+ end
170
+
143
171
  def status
144
172
  return :failed if failed?
145
173
  return :running if running?
@@ -157,8 +185,39 @@ module Dwf
157
185
  @stopped = false
158
186
  end
159
187
 
188
+ def leaf_nodes
189
+ jobs.select(&:leaf?)
190
+ end
191
+
192
+ def output_payload
193
+ leaf_nodes.map do |node|
194
+ data = node.output_payload
195
+ next if data.nil?
196
+
197
+ data
198
+ end.compact
199
+ end
200
+
160
201
  private
161
202
 
203
+ def build_node(klass, options)
204
+ if klass < Dwf::Workflow
205
+ node = options[:params].nil? ? klass.new : klass.new(options[:params])
206
+ node.parent_id = id
207
+ node.callback_type = SK_BATCH
208
+ node.save
209
+ node
210
+ else
211
+ klass.new(
212
+ workflow_id: id,
213
+ id: client.build_job_id(id, klass.to_s),
214
+ params: options.fetch(:params, {}),
215
+ queue: options[:queue],
216
+ callback_type: callback_type
217
+ )
218
+ end
219
+ end
220
+
162
221
  def initial_jobs
163
222
  jobs.select(&:no_dependencies?)
164
223
  end
@@ -168,16 +227,52 @@ module Dwf
168
227
  resolve_dependencies
169
228
  end
170
229
 
230
+ def find_node(node_name)
231
+ if Utils.workflow_name?(node_name)
232
+ find_subworkflow(node_name)
233
+ else
234
+ find_job(node_name)
235
+ end
236
+ end
237
+
238
+ def find_subworkflow(node_name)
239
+ fname, = node_name.split('|')
240
+ jobs.find { |j| j.klass.name == fname }
241
+ end
242
+
171
243
  def resolve_dependencies
172
244
  @dependencies.each do |dependency|
173
- from = find_job(dependency[:from])
174
- to = find_job(dependency[:to])
245
+ from = find_node(dependency[:from])
246
+ to = find_node(dependency[:to])
175
247
 
176
- to.incoming << dependency[:from]
177
- from.outgoing << dependency[:to]
248
+ to.incoming << from.name
249
+ from.outgoing << to.name
178
250
  end
179
251
  end
180
252
 
253
+ def invalid_callback?
254
+ cb_build_in? && jobs.any? { |job| job.class < Workflow }
255
+ end
256
+
257
+ def build_payloads
258
+ return unless sub_workflow?
259
+
260
+ data = incoming.map do |job_name|
261
+ next if Utils.workflow_name?(job_name)
262
+
263
+ node = client.find_node(job_name, parent_id)
264
+ next if node.output_payload.nil?
265
+
266
+ {
267
+ id: node.name,
268
+ class: node.klass.to_s,
269
+ output: node.output_payload
270
+ }
271
+ end.compact
272
+
273
+ data.empty? ? nil : data
274
+ end
275
+
181
276
  def build_dependencies_structure(node, options)
182
277
  deps_after = [*options[:after]]
183
278
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'spec_helper'
4
4
  require 'mock_redis'
5
+ FirstWorkflow = Class.new(Dwf::Workflow)
6
+ SecondWorkflow = Class.new(Dwf::Workflow)
5
7
 
6
8
  describe Dwf::Client, client: true do
7
9
  let(:client) { described_class.new }
@@ -30,12 +32,12 @@ describe Dwf::Client, client: true do
30
32
  end
31
33
 
32
34
  context 'find by item name' do
33
- it {
35
+ it do
34
36
  item = client.find_job(workflow_id, job.name)
35
37
  expect(item.workflow_id).to eq workflow_id
36
38
  expect(item.id).to eq id
37
39
  expect(item.name).to eq job.name
38
- }
40
+ end
39
41
  end
40
42
  end
41
43
 
@@ -62,6 +64,47 @@ describe Dwf::Client, client: true do
62
64
  end
63
65
  end
64
66
 
67
+ describe '#find_node' do
68
+ context 'find job' do
69
+ let!(:job) do
70
+ j = Dwf::Item.new(workflow_id: workflow_id, id: id)
71
+ j.persist!
72
+ j
73
+ end
74
+
75
+ it do
76
+ item = client.find_node(Dwf::Item.name, workflow_id)
77
+ expect(item.workflow_id).to eq workflow_id
78
+ expect(item.id).to eq id
79
+ expect(item.name).to eq job.name
80
+ end
81
+ end
82
+
83
+ context 'find_workflow' do
84
+ let!(:wf1) { FirstWorkflow.create }
85
+ let!(:wf2) do
86
+ wf = SecondWorkflow.new
87
+ wf.parent_id = wf1.id
88
+ wf.save
89
+ wf
90
+ end
91
+
92
+ context 'find with class name and parent id' do
93
+ it do
94
+ wf = client.find_node(wf2.class.name, wf1.id)
95
+ expect(wf).to be_kind_of(SecondWorkflow)
96
+ end
97
+ end
98
+
99
+ context 'find with name and parent id' do
100
+ it do
101
+ wf = client.find_node(wf2.name, wf1.id)
102
+ expect(wf).to be_kind_of(SecondWorkflow)
103
+ end
104
+ end
105
+ end
106
+ end
107
+
65
108
  describe '#persist_job' do
66
109
  let!(:job) { Dwf::Item.new(workflow_id: workflow_id, id: id) }
67
110
 
@@ -76,13 +119,44 @@ describe Dwf::Client, client: true do
76
119
  end
77
120
  end
78
121
 
122
+ describe '#find_sub_workflow' do
123
+ let!(:wf1) { FirstWorkflow.create }
124
+ let!(:wf2) do
125
+ wf = SecondWorkflow.new
126
+ wf.parent_id = wf1.id
127
+ wf.save
128
+ wf
129
+ end
130
+
131
+ it do
132
+ wf = client.find_sub_workflow(wf2.class.name, wf1.id)
133
+ expect(wf).to be_kind_of(SecondWorkflow)
134
+ end
135
+ end
136
+
137
+ describe '#sub_workflows' do
138
+ let!(:wf1) { FirstWorkflow.create }
139
+ let!(:wf2) do
140
+ wf = SecondWorkflow.new
141
+ wf.parent_id = wf1.id
142
+ wf.save
143
+ wf
144
+ end
145
+
146
+ it do
147
+ wfs = client.sub_workflows(wf1.id)
148
+ expect(wfs).not_to be_empty
149
+ expect(wfs.first).to be_kind_of(SecondWorkflow)
150
+ end
151
+ end
152
+
79
153
  describe '#persist_workflow' do
80
154
  let(:workflow) { Dwf::Workflow.new }
81
155
 
82
156
  it do
83
157
  expect(redis.exists?("dwf.workflows.#{workflow.id}")).to be_falsy
84
158
  client.persist_workflow(workflow)
85
- expect(redis.exists?("dwf.workflows.#{workflow.id}")).to be_truthy
159
+ expect(redis.keys("dwf.workflows.#{workflow.id}*").any?).to be_truthy
86
160
  end
87
161
  end
88
162
 
@@ -20,7 +20,8 @@ describe Dwf::Item, item: true do
20
20
  klass: 'Dwf::Item',
21
21
  started_at: started_at,
22
22
  finished_at: finished_at,
23
- callback_type: Dwf::Workflow::BUILD_IN
23
+ callback_type: Dwf::Workflow::BUILD_IN,
24
+ payloads: nil
24
25
  }
25
26
  end
26
27
  let!(:item) { described_class.new(options) }
@@ -89,7 +90,7 @@ describe Dwf::Item, item: true do
89
90
  before do
90
91
  allow(Dwf::Client).to receive(:new).and_return client_double
91
92
  allow(client_double)
92
- .to receive(:find_job).and_return a_item
93
+ .to receive(:find_node).and_return a_item
93
94
  end
94
95
 
95
96
  context 'parent jobs already finished' do
@@ -98,8 +99,8 @@ describe Dwf::Item, item: true do
98
99
  it do
99
100
  expect(item.parents_succeeded?).to be_truthy
100
101
  expect(client_double)
101
- .to have_received(:find_job)
102
- .with(workflow_id, incoming.first)
102
+ .to have_received(:find_node)
103
+ .with(incoming.first, workflow_id)
103
104
  end
104
105
  end
105
106
 
@@ -109,8 +110,8 @@ describe Dwf::Item, item: true do
109
110
  it do
110
111
  expect(item.parents_succeeded?).to be_falsy
111
112
  expect(client_double)
112
- .to have_received(:find_job)
113
- .with(workflow_id, incoming.first)
113
+ .to have_received(:find_node)
114
+ .with(incoming.first, workflow_id)
114
115
  end
115
116
  end
116
117
  end
@@ -189,8 +190,9 @@ describe Dwf::Item, item: true do
189
190
  end
190
191
 
191
192
  describe '#payloads' do
192
- let(:incoming) { ["A|#{SecureRandom.uuid}"] }
193
- let(:client_double) { double(find_job: nil) }
193
+ let(:incoming) { ["Dwf::Item|#{SecureRandom.uuid}", "Dwf::Workflow|#{workflow_id}"] }
194
+ let(:client_double) { double(find_job: nil, build_workflow_id: workflow_id) }
195
+ let(:workflow) { Dwf::Workflow.new }
194
196
  let!(:a_item) do
195
197
  described_class.new(
196
198
  workflow_id: SecureRandom.uuid,
@@ -203,7 +205,11 @@ describe Dwf::Item, item: true do
203
205
  before do
204
206
  allow(Dwf::Client).to receive(:new).and_return client_double
205
207
  allow(client_double)
206
- .to receive(:find_job).and_return a_item
208
+ .to receive(:find_node)
209
+ .with(incoming.first, workflow_id).and_return a_item
210
+ allow(client_double)
211
+ .to receive(:find_node)
212
+ .with(incoming.last, workflow_id).and_return workflow
207
213
  end
208
214
 
209
215
  it do
@@ -212,9 +218,42 @@ describe Dwf::Item, item: true do
212
218
  class: a_item.class.name,
213
219
  id: a_item.name,
214
220
  output: 1
221
+ },
222
+ {
223
+ class: workflow.class.name,
224
+ id: workflow.name,
225
+ output: []
215
226
  }
216
227
  ]
217
- expect(item.payloads).to eq expected_payload
228
+ expect(item.payloads).to match_array expected_payload
229
+ end
230
+ end
231
+
232
+ describe '#start_batch!' do
233
+ let(:callback_double) { double(start: nil) }
234
+ let(:client_double) { double(persist_job: nil) }
235
+
236
+ before do
237
+ allow(Dwf::Client).to receive(:new).and_return client_double
238
+ allow(Dwf::Callback).to receive(:new).and_return callback_double
239
+ item.start_batch!
240
+ end
241
+
242
+ it do
243
+ expect(callback_double).to have_received(:start).with(item)
244
+ expect(item.enqueued_at).not_to be_nil
245
+ end
246
+ end
247
+
248
+ describe '#leaf?' do
249
+ context 'when item has outgoing item' do
250
+ let(:outgoing) { ["Dwf::Item|#{SecureRandom.uuid}"] }
251
+ it { expect(item.leaf?).to be_falsy }
252
+ end
253
+
254
+ context 'when item does not have outgoing item' do
255
+ let(:outgoing) { [] }
256
+ it { expect(item.leaf?).to be_truthy }
218
257
  end
219
258
  end
220
259
  end
@@ -20,4 +20,13 @@ describe Dwf::Utils, utils: true do
20
20
 
21
21
  it { expect(described_class.symbolize_keys(hash)).to eq expected }
22
22
  end
23
+
24
+ describe "#workflow_name?" do
25
+ FirstWorkflow = Class.new(Dwf::Workflow)
26
+ FirstJob = Class.new(Dwf::Item)
27
+
28
+ it { expect(described_class.workflow_name?(Dwf::Workflow.name)).to be_truthy }
29
+ it { expect(described_class.workflow_name?(FirstWorkflow.name)).to be_truthy }
30
+ it { expect(described_class.workflow_name?(FirstJob.name)).to be_falsy }
31
+ end
23
32
  end