Skip to content

Commit 98e897c

Browse files
authored
Merge pull request #5 from jobready/AV-3601-initialise-after-lock
[AV-3601] Initialise ivars after lock and do not execute decisions/entries if finished
2 parents c87b6c6 + 0b0d764 commit 98e897c

File tree

3 files changed

+144
-41
lines changed

3 files changed

+144
-41
lines changed

lib/decision_tree/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module DecisionTree
2-
VERSION = "0.1.0"
2+
VERSION = "0.1.1"
33
end

lib/decision_tree/workflow.rb

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,18 @@ class MethodNotDefinedError < StandardError; end
1717
def initialize(store=nil)
1818
@store = store || DecisionTree::Store.new
1919
@steps = []
20-
initialize_persistent_state
2120
@proxy = DecisionTree::Proxy.new(self)
2221

23-
if finished?
24-
@steps = store.fetch_steps
25-
else
26-
execute_workflow
22+
store.start_workflow do
23+
initialize_persistent_state
24+
25+
if finished?
26+
@steps = store.fetch_steps
27+
else
28+
execute_workflow
29+
end
30+
31+
persist_state!
2732
end
2833
end
2934

@@ -46,20 +51,15 @@ def finished?
4651
# Actually executes the workflow steps, by executing all the steps from
4752
# either the start, or all previously reached entry points
4853
def execute_workflow
49-
# We're using pessimistic locking here, so this will block until an
50-
# exclusive lock can be obtained on the change.
51-
store.start_workflow do
52-
catch :exit do
53-
if @entry_points.empty?
54-
send(:__start_workflow)
55-
else
56-
# TODO: This should fail silently if an entry point is no longer
57-
# defined, this will allow for modification of the workflows with
58-
# existing changes in the DB.
59-
@entry_points.each { |ep| send(ep) }
60-
end
54+
catch :exit do
55+
if @entry_points.empty?
56+
send(:__start_workflow)
57+
else
58+
# TODO: This should fail silently if an entry point is no longer
59+
# defined, this will allow for modification of the workflows with
60+
# existing changes in the DB.
61+
@entry_points.each { |ep| send(ep) }
6162
end
62-
persist_state!
6363
end
6464
end
6565

@@ -108,6 +108,8 @@ def self.decision(method_name, &block)
108108
fail YesAndNoRequiredError unless yes_block && no_block
109109

110110
define_method(method_name) do
111+
return if finished?
112+
111113
if send(aliased_method_name)
112114
@steps << DecisionTree::Step.new(method_name, 'YES')
113115
@proxy.instance_eval(&yes_block)
@@ -128,6 +130,8 @@ def self.entry(method_name, &block)
128130
aliased_method_name = alias_method_name(method_name)
129131

130132
define_method(method_name) do
133+
return self if finished?
134+
131135
@entry_points << method_name.to_s
132136
@steps << DecisionTree::Step.new('Entry Point', method_name.to_s)
133137
send(aliased_method_name)
@@ -137,6 +141,7 @@ def self.entry(method_name, &block)
137141
end
138142
persist_state!
139143
end
144+
140145
self
141146
end
142147
end

spec/decision_tree/workflow_spec.rb

Lines changed: 120 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ def decision_method
4949
decision_method
5050
end
5151
end
52-
expect_any_instance_of(TestWorkflow).to receive(:__decision_method).once.and_return(true)
5352
end
5453

5554
it 'calls only the yes block' do
@@ -75,7 +74,6 @@ def decision_method
7574
decision_method
7675
end
7776
end
78-
expect_any_instance_of(TestWorkflow).to receive(:__decision_method).once.and_return(false)
7977
end
8078

8179
it 'calls only the no block' do
@@ -117,15 +115,47 @@ def always_true
117115
end
118116
end
119117
end
118+
119+
context 'when the workflow is already finished' do
120+
before do
121+
class TestWorkflow < DecisionTree::Workflow
122+
def decision_method
123+
end
124+
125+
decision :decision_method do
126+
yes { }
127+
no { }
128+
end
129+
end
130+
end
131+
132+
it 'does not execute the decision' do
133+
allow_any_instance_of(TestWorkflow).to receive(:finished?).and_return(true)
134+
expect_any_instance_of(TestWorkflow).not_to receive(:__decision_method)
135+
TestWorkflow.new(store).send(:decision_method)
136+
end
137+
end
120138
end
121139

122140
describe '.entry' do
123141
before do
124142
class TestWorkflow < DecisionTree::Workflow
143+
def always_true
144+
true
145+
end
146+
147+
def non_idempotent_action!
148+
end
149+
125150
def test_entry
126151
end
127152

128-
entry(:test_entry) {}
153+
decision :always_true do
154+
yes { non_idempotent_action! }
155+
no { exit }
156+
end
157+
158+
entry(:test_entry) { always_true }
129159
start {}
130160
end
131161
end
@@ -156,6 +186,35 @@ def test_entry
156186
TestWorkflow.new(store)
157187
end
158188
end
189+
190+
context 'for a store that updates state before yielding to workflow (ie locking)' do
191+
subject { TestWorkflow.new(store) }
192+
let(:store) { TestStore.new }
193+
194+
before do
195+
class TestStore < DecisionTree::Store
196+
def start_workflow(&block)
197+
self.state = '__start_workflow:non_idempotent_action!'
198+
yield
199+
end
200+
end
201+
end
202+
203+
it 'does not call the non-idempotent method again' do
204+
expect_any_instance_of(TestWorkflow).to_not receive(:non_idempotent_action!)
205+
subject.test_entry
206+
end
207+
end
208+
209+
context 'when the workflow is already finished' do
210+
subject { TestWorkflow.new(store) }
211+
212+
it 'does not execute the entry point' do
213+
allow_any_instance_of(TestWorkflow).to receive(:finished?).and_return(true)
214+
expect_any_instance_of(TestWorkflow).not_to receive(:__test_entry)
215+
subject.test_entry
216+
end
217+
end
159218
end
160219

161220
describe '.start' do
@@ -188,33 +247,72 @@ def decision_method
188247
end
189248

190249
describe '.initialize' do
250+
subject { TestWorkflow.new(store) }
251+
252+
let(:store) { TestStore.new }
253+
191254
before do
192255
class TestWorkflow < DecisionTree::Workflow
193-
end
256+
def always_true
257+
true
258+
end
259+
260+
decision :always_true do
261+
yes { non_idempotent_action! }
262+
no { exit }
263+
end
194264

195-
allow_any_instance_of(TestWorkflow).to receive(:finished?) { finished }
265+
start { always_true }
266+
end
196267
end
197268

198-
subject { TestWorkflow.new(store) }
269+
context 'for store that simply yields to the workflow' do
270+
before do
271+
class TestStore < DecisionTree::Store
272+
def start_workflow(&block)
273+
yield
274+
end
275+
end
199276

200-
context 'when workflow not previously completed' do
201-
let(:finished) { false }
202-
specify 'executes the workflow' do
203-
expect_any_instance_of(TestWorkflow).to receive(:execute_workflow)
204-
subject
277+
allow_any_instance_of(TestWorkflow).to receive(:finished?) { finished }
205278
end
206-
end
207279

208-
context 'when workflow previously completed' do
209-
let(:finished) { true }
280+
context 'when workflow previously completed' do
281+
let(:finished) { true }
210282

211-
specify 'does not execute the workflow' do
212-
expect_any_instance_of(TestWorkflow).to_not receive(:execute_workflow)
213-
subject
283+
it 'does not execute the workflow' do
284+
expect_any_instance_of(TestWorkflow).to_not receive(:execute_workflow)
285+
subject
286+
end
287+
288+
it 'fetches previously executed steps from the store' do
289+
expect(store).to receive(:fetch_steps)
290+
subject
291+
end
292+
end
293+
294+
context 'when workflow not previously completed' do
295+
let(:finished) { false }
296+
297+
it 'executes the workflow' do
298+
expect_any_instance_of(TestWorkflow).to receive(:execute_workflow)
299+
subject
300+
end
301+
end
302+
end
303+
304+
context 'for a store that updates state before yielding to workflow (ie locking)' do
305+
before do
306+
class TestStore < DecisionTree::Store
307+
def start_workflow(&block)
308+
self.state = '__start_workflow:non_idempotent_action!'
309+
yield
310+
end
311+
end
214312
end
215313

216-
specify 'fetches previously executed steps from the store' do
217-
expect(store).to receive(:fetch_steps)
314+
it 'does not call the non-idempotent method again' do
315+
expect_any_instance_of(TestWorkflow).to_not receive(:non_idempotent_action!)
218316
subject
219317
end
220318
end
@@ -232,7 +330,7 @@ class TestWorkflow < DecisionTree::Workflow
232330

233331
let(:workflow) { TestWorkflow.new(store) }
234332

235-
specify 'records the finish call' do
333+
it 'records the finish call' do
236334
finish!
237335
expect(subject).to include('finish!')
238336
end
@@ -249,7 +347,7 @@ class TestWorkflow < DecisionTree::Workflow
249347
end
250348
end
251349

252-
specify 'calls finish! on the workflow' do
350+
it 'calls finish! on the workflow' do
253351
expect_any_instance_of(TestWorkflow).to receive(:finish!)
254352
workflow
255353
end
@@ -271,7 +369,7 @@ def decision_method
271369
end
272370
end
273371

274-
specify 'calls finish! on the workflow' do
372+
it 'calls finish! on the workflow' do
275373
expect_any_instance_of(TestWorkflow).to receive(:finish!)
276374
workflow
277375
end

0 commit comments

Comments
 (0)