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