dynflow 2.0.1 → 2.0.2
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 +4 -4
- data/Gemfile +4 -1
- data/dynflow.gemspec +3 -2
- data/lib/dynflow/execution_plan/steps/finalize_step.rb +2 -0
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/world/invalidation.rb +49 -10
- data/lib/dynflow/world.rb +6 -7
- data/lib/dynflow.rb +1 -0
- data/test/abnormal_states_recovery_test.rb +32 -5
- data/test/action_test.rb +5 -11
- data/test/bats/sidekiq-orchestrator.bats +4 -1
- data/test/rescue_test.rb +19 -0
- data/test/world_test.rb +24 -0
- metadata +26 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4219e6a5f8d1e1829a43c97bb12a406c27e3a14fd9be7d29d795d63580789f9c
|
|
4
|
+
data.tar.gz: 3d9c6427c2bd1c2483844ca35e4b4a277d41d42113eac775aa9555e033a45e43
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d62d9d27f2dec86e8c00f2d62bb51e3c81ae5c816fd3c59cde34518cffe7b77dc994acc999d5effd16bf19c73f35f12b6cec9ee6f9f491ba30dbb46eae149dc7
|
|
7
|
+
data.tar.gz: a6dcded79039250e858b1c89d2d0b3793c1de3e41408d9c60d9e49a053984f849c990940c674a367ba4a6a42429ff06f571f915d6de8206cc27a9e967a2c28ec
|
data/Gemfile
CHANGED
|
@@ -4,8 +4,11 @@ source 'https://rubygems.org'
|
|
|
4
4
|
|
|
5
5
|
gemspec
|
|
6
6
|
|
|
7
|
+
# multi_json 1.21.2 fixes json 3 compatibility, but requires Ruby 3.2+
|
|
8
|
+
gem 'json', '< 3.0' if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.2')
|
|
9
|
+
|
|
7
10
|
group :concurrent_ruby_ext, optional: ENV.key?('CI') && ENV['CONCURRENT_RUBY_EXT'] != 'true' do
|
|
8
|
-
gem 'concurrent-ruby-ext', '~> 1.
|
|
11
|
+
gem 'concurrent-ruby-ext', '~> 1.3.0'
|
|
9
12
|
end
|
|
10
13
|
|
|
11
14
|
group :pry, optional: ENV.key?('CI') do
|
data/dynflow.gemspec
CHANGED
|
@@ -21,9 +21,10 @@ Gem::Specification.new do |s|
|
|
|
21
21
|
s.required_ruby_version = '>= 3.0.0'
|
|
22
22
|
|
|
23
23
|
s.add_dependency "algebrick", '~> 0.7.0'
|
|
24
|
-
s.add_dependency "concurrent-ruby", '
|
|
25
|
-
s.add_dependency "concurrent-ruby-edge", '~> 0.
|
|
24
|
+
s.add_dependency "concurrent-ruby", '>= 1.1.3', '< 2.0'
|
|
25
|
+
s.add_dependency "concurrent-ruby-edge", '~> 0.7.0'
|
|
26
26
|
s.add_dependency "csv", "~> 3.1"
|
|
27
|
+
s.add_dependency 'logger'
|
|
27
28
|
s.add_dependency "msgpack", '~> 1.3', '>= 1.3.3'
|
|
28
29
|
s.add_dependency "multi_json"
|
|
29
30
|
s.add_dependency "sequel", '>= 4.0.0'
|
data/lib/dynflow/version.rb
CHANGED
|
@@ -52,17 +52,9 @@ module Dynflow
|
|
|
52
52
|
|
|
53
53
|
def invalidate_planning_lock(planning_lock)
|
|
54
54
|
with_valid_execution_plan_for_lock(planning_lock) do |plan|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
state = if plan.plan_steps.any? && plan.plan_steps.all? { |step| step.state == :success }
|
|
58
|
-
:planned
|
|
59
|
-
else
|
|
60
|
-
:stopped
|
|
61
|
-
end
|
|
62
|
-
plan.update_state(state) if plan.state != state
|
|
63
|
-
|
|
55
|
+
state = invalidate_planning_plan(plan)
|
|
64
56
|
coordinator.release(planning_lock)
|
|
65
|
-
execute(plan.id) if
|
|
57
|
+
execute(plan.id) if state == :planned
|
|
66
58
|
end
|
|
67
59
|
end
|
|
68
60
|
|
|
@@ -140,6 +132,7 @@ module Dynflow
|
|
|
140
132
|
invalidate_execution_lock(lock)
|
|
141
133
|
end
|
|
142
134
|
end
|
|
135
|
+
execution_plans_validity_check
|
|
143
136
|
pruned = connector.prune_undeliverable_envelopes(self)
|
|
144
137
|
logger.error("Pruned #{pruned} undeliverable envelopes") unless pruned.zero?
|
|
145
138
|
world_invalidation_result.values.select { |result| result == :invalidated }.size
|
|
@@ -198,8 +191,54 @@ module Dynflow
|
|
|
198
191
|
return orphaned_locks
|
|
199
192
|
end
|
|
200
193
|
|
|
194
|
+
# Invalidates execution plans left in planning without a planning lock.
|
|
195
|
+
# Acquiring a new planning lock prevents recovery from racing with another
|
|
196
|
+
# validity check or a planning process which has just acquired the lock.
|
|
197
|
+
#
|
|
198
|
+
# @return [Array<String>] ids of invalidated execution plans
|
|
199
|
+
def execution_plans_validity_check
|
|
200
|
+
planning_lock_ids = coordinator.find_locks(class: Coordinator::PlanningLock.name).map(&:execution_plan_id)
|
|
201
|
+
planning_plans = persistence.find_execution_plans(filters: { state: 'planning' })
|
|
202
|
+
invalidated_plan_ids = planning_plans.filter_map do |plan|
|
|
203
|
+
next if planning_lock_ids.include?(plan.id)
|
|
204
|
+
|
|
205
|
+
plan.id if invalidate_planning_plan_without_lock(plan.id)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
unless invalidated_plan_ids.empty?
|
|
209
|
+
logger.error "execution plans in planning without a planning lock found and invalidated: #{invalidated_plan_ids.inspect}"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
invalidated_plan_ids
|
|
213
|
+
end
|
|
214
|
+
|
|
201
215
|
private
|
|
202
216
|
|
|
217
|
+
def invalidate_planning_plan_without_lock(execution_plan_id)
|
|
218
|
+
state = nil
|
|
219
|
+
planning_lock = Coordinator::PlanningLock.new(self, execution_plan_id)
|
|
220
|
+
coordinator.acquire(planning_lock) do
|
|
221
|
+
plan = persistence.load_execution_plan(execution_plan_id)
|
|
222
|
+
state = invalidate_planning_plan(plan) if plan.state == :planning
|
|
223
|
+
end
|
|
224
|
+
execute(execution_plan_id) if state == :planned
|
|
225
|
+
!state.nil?
|
|
226
|
+
rescue Coordinator::LockError, KeyError
|
|
227
|
+
false
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def invalidate_planning_plan(plan)
|
|
231
|
+
plan.steps.values.each { |step| invalidate_step step }
|
|
232
|
+
|
|
233
|
+
state = if plan.plan_steps.any? && plan.plan_steps.all? { |step| step.state == :success }
|
|
234
|
+
:planned
|
|
235
|
+
else
|
|
236
|
+
:stopped
|
|
237
|
+
end
|
|
238
|
+
plan.update_state(state) if plan.state != state
|
|
239
|
+
state
|
|
240
|
+
end
|
|
241
|
+
|
|
203
242
|
def invalidate_step(step)
|
|
204
243
|
if step.state == :running
|
|
205
244
|
step.error = ExecutionPlan::Steps::Error.new("Abnormal termination (previous state: #{step.state})")
|
data/lib/dynflow/world.rb
CHANGED
|
@@ -355,21 +355,20 @@ module Dynflow
|
|
|
355
355
|
rescue Dynflow::Errors::FatalPersistenceError => e
|
|
356
356
|
nil
|
|
357
357
|
end
|
|
358
|
-
@terminated.resolve
|
|
359
358
|
true
|
|
360
359
|
rescue => e
|
|
361
360
|
logger.fatal(e)
|
|
362
361
|
end
|
|
363
362
|
end
|
|
364
|
-
@terminating = Concurrent::Promises.
|
|
363
|
+
@terminating = Concurrent::Promises.resolvable_future
|
|
364
|
+
Thread.new do
|
|
365
365
|
termination_future.wait(termination_timeout)
|
|
366
|
-
end.on_resolution do
|
|
367
366
|
@terminated.resolve
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
end
|
|
367
|
+
logger.info 'World terminated, exiting.'
|
|
368
|
+
@terminating.fulfill(true)
|
|
369
|
+
Kernel.exit if @exit_on_terminate.true?
|
|
372
370
|
end
|
|
371
|
+
@terminating
|
|
373
372
|
end
|
|
374
373
|
end
|
|
375
374
|
|
data/lib/dynflow.rb
CHANGED
|
@@ -137,21 +137,48 @@ module Dynflow
|
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
describe 'planning locks' do
|
|
140
|
+
it 'executes fully planned execution plans left in planning without a lock' do
|
|
141
|
+
plan = client_world.plan(Support::DummyExample::Dummy)
|
|
142
|
+
plan.set_state(:planning, true)
|
|
143
|
+
plan.save
|
|
144
|
+
|
|
145
|
+
client_world_2.perform_validity_checks
|
|
146
|
+
|
|
147
|
+
wait_for do
|
|
148
|
+
plan = client_world_2.persistence.load_execution_plan(plan.id)
|
|
149
|
+
plan.state == :stopped
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it 'stops incompletely planned execution plans without a lock' do
|
|
154
|
+
plan = client_world.plan(Support::DummyExample::Dummy)
|
|
155
|
+
plan.set_state(:planning, true)
|
|
156
|
+
plan.save
|
|
157
|
+
step = plan.plan_steps.first
|
|
158
|
+
step.set_state(:pending, true)
|
|
159
|
+
step.save
|
|
160
|
+
|
|
161
|
+
client_world_2.perform_validity_checks
|
|
162
|
+
|
|
163
|
+
plan = client_world_2.persistence.load_execution_plan(plan.id)
|
|
164
|
+
_(plan.state).must_equal :stopped
|
|
165
|
+
end
|
|
166
|
+
|
|
140
167
|
it 'releases orphaned planning locks and executes associated execution plans' do
|
|
141
168
|
plan = client_world.plan(Support::DummyExample::Dummy)
|
|
142
169
|
plan.set_state(:planning, true)
|
|
143
170
|
plan.save
|
|
144
171
|
client_world.coordinator.acquire Coordinator::PlanningLock.new(client_world, plan.id)
|
|
145
172
|
executor_world.invalidate(client_world.registered_world)
|
|
146
|
-
expected_locks = ["lock world-invalidation:#{client_world.id}",
|
|
147
|
-
"unlock execution-plan:#{plan.id}", # planning lock
|
|
148
|
-
"lock execution-plan:#{plan.id}", # execution lock
|
|
149
|
-
"unlock world-invalidation:#{client_world.id}"]
|
|
150
|
-
_(executor_world.coordinator.adapter.lock_log).must_equal(expected_locks)
|
|
151
173
|
wait_for do
|
|
152
174
|
plan = client_world_2.persistence.load_execution_plan(plan.id)
|
|
153
175
|
plan.state == :stopped
|
|
154
176
|
end
|
|
177
|
+
lock_log = executor_world.coordinator.adapter.lock_log
|
|
178
|
+
_(lock_log).must_include "lock world-invalidation:#{client_world.id}"
|
|
179
|
+
_(lock_log).must_include "unlock execution-plan:#{plan.id}" # planning lock
|
|
180
|
+
_(lock_log).must_include "lock execution-plan:#{plan.id}" # execution lock
|
|
181
|
+
_(lock_log).must_include "unlock world-invalidation:#{client_world.id}"
|
|
155
182
|
end
|
|
156
183
|
|
|
157
184
|
it 'releases orphaned planning locks and stops associated execution plans which did not finish planning' do
|
data/test/action_test.rb
CHANGED
|
@@ -569,11 +569,8 @@ module Dynflow
|
|
|
569
569
|
polling_plan.sub_plans_count == total
|
|
570
570
|
end
|
|
571
571
|
|
|
572
|
-
# Moving the clock to make the parent check on sub plans
|
|
573
|
-
_(clock.pending_pings.count).must_equal 1
|
|
574
|
-
clock.progress
|
|
575
|
-
|
|
576
572
|
wait_for('the parent to realise the sub plans failed') do
|
|
573
|
+
clock.progress
|
|
577
574
|
polling_plan = world.persistence.load_execution_plan(triggered_plan.id)
|
|
578
575
|
polling_plan.state == :paused
|
|
579
576
|
end
|
|
@@ -586,11 +583,8 @@ module Dynflow
|
|
|
586
583
|
polling_plan.sub_plans_count == 2 * total
|
|
587
584
|
end
|
|
588
585
|
|
|
589
|
-
# Move the clock again
|
|
590
|
-
_(clock.pending_pings.count).must_equal 1
|
|
591
|
-
clock.progress
|
|
592
|
-
|
|
593
586
|
wait_for('everything to finish successfully') do
|
|
587
|
+
clock.progress
|
|
594
588
|
polling_plan = world.persistence.load_execution_plan(triggered_plan.id)
|
|
595
589
|
polling_plan.state == :stopped && polling_plan.result == :success
|
|
596
590
|
end
|
|
@@ -611,7 +605,7 @@ module Dynflow
|
|
|
611
605
|
end
|
|
612
606
|
|
|
613
607
|
# Moving the clock to make the parent check on sub plans
|
|
614
|
-
|
|
608
|
+
wait_for('the parent poll to be scheduled') { clock.pending_pings.count == 1 }
|
|
615
609
|
clock.progress
|
|
616
610
|
_(clock.pending_pings.count).must_equal 0
|
|
617
611
|
|
|
@@ -694,7 +688,7 @@ module Dynflow
|
|
|
694
688
|
plan.sub_plans_count == total &&
|
|
695
689
|
plan.sub_plans.all? { |sub| sub.result == :success }
|
|
696
690
|
end
|
|
697
|
-
|
|
691
|
+
wait_for('the parent poll to be scheduled') { clock.pending_pings.count == 1 }
|
|
698
692
|
clock.progress
|
|
699
693
|
wait_for do
|
|
700
694
|
plan = world.persistence.load_execution_plan(plan.id)
|
|
@@ -716,7 +710,7 @@ module Dynflow
|
|
|
716
710
|
plan.entry_action.output[:planning_finished] == 1
|
|
717
711
|
end
|
|
718
712
|
# Poll was set during #initiate
|
|
719
|
-
|
|
713
|
+
wait_for('the parent poll to be scheduled') { clock.pending_pings.count == 1 }
|
|
720
714
|
|
|
721
715
|
# Wait for the sub plans to finish
|
|
722
716
|
wait_for do
|
|
@@ -96,7 +96,10 @@ teardown() {
|
|
|
96
96
|
wait_for 5 1 grep 'dynflow: Finished performing validity checks' "$(bg_output_file o1)"
|
|
97
97
|
|
|
98
98
|
podman stop "$POSTGRES_CONTAINER_NAME"
|
|
99
|
-
|
|
99
|
+
# With concurrent-ruby 1.3 the persistence error surfaces after
|
|
100
|
+
# MAX_RETRIES polling rounds, which pushes the termination log past a
|
|
101
|
+
# 60s window (observed ~58s); allow a generous margin.
|
|
102
|
+
wait_for 180 1 grep 'dynflow: World terminated, exiting.' "$(bg_output_file o1)"
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
@test "active orchestrator can withstand temporary pg connection drop" {
|
data/test/rescue_test.rb
CHANGED
|
@@ -34,6 +34,25 @@ module Dynflow
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
describe 'when skipping a run step fails' do
|
|
38
|
+
let :execution_plan do
|
|
39
|
+
execute(Example::ActionWithSkip, 1, :error_on_skip)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'allows retrying the skip' do
|
|
43
|
+
2.times do
|
|
44
|
+
plan = world.persistence.load_execution_plan(execution_plan.id)
|
|
45
|
+
plan.skip(plan.failed_steps.first)
|
|
46
|
+
world.execute(plan.id).value
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
plan = world.persistence.load_execution_plan(execution_plan.id)
|
|
50
|
+
_(plan.state).must_equal :paused
|
|
51
|
+
_(plan.run_steps.first.state).must_equal :error
|
|
52
|
+
_(plan.finalize_steps.first.state).must_equal :skipped
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
37
56
|
describe 'of simple skippable action in finalize phase' do
|
|
38
57
|
let :execution_plan do
|
|
39
58
|
execute(Example::ActionWithSkip, 1, :error_on_finalize)
|
data/test/world_test.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'test_helper'
|
|
4
4
|
require 'fileutils'
|
|
5
|
+
require 'mocha/minitest'
|
|
5
6
|
|
|
6
7
|
module Dynflow
|
|
7
8
|
module WorldTest
|
|
@@ -50,6 +51,29 @@ module Dynflow
|
|
|
50
51
|
terminated_event.wait(10)
|
|
51
52
|
_(terminated_event.resolved?).must_equal true
|
|
52
53
|
end
|
|
54
|
+
|
|
55
|
+
it 'finishes when the global IO executor is saturated' do
|
|
56
|
+
io_executor = Concurrent::FixedThreadPool.new(1)
|
|
57
|
+
io_started = Concurrent::Event.new
|
|
58
|
+
io_release = Concurrent::Event.new
|
|
59
|
+
saturated_world = WorldFactory.create_world { |config| config.termination_timeout = 0.1 }
|
|
60
|
+
blocker = Concurrent::Promises.future_on(io_executor) do
|
|
61
|
+
io_started.set
|
|
62
|
+
io_release.wait
|
|
63
|
+
end
|
|
64
|
+
assert io_started.wait(1)
|
|
65
|
+
Concurrent.stubs(:global_io_executor).returns(io_executor)
|
|
66
|
+
|
|
67
|
+
termination = saturated_world.terminate
|
|
68
|
+
|
|
69
|
+
assert termination.wait(1)
|
|
70
|
+
_(saturated_world.terminated.resolved?).must_equal true
|
|
71
|
+
ensure
|
|
72
|
+
io_release&.set
|
|
73
|
+
blocker&.wait(1)
|
|
74
|
+
io_executor&.shutdown
|
|
75
|
+
io_executor&.wait_for_termination(10)
|
|
76
|
+
end
|
|
53
77
|
end
|
|
54
78
|
end
|
|
55
79
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dynflow
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ivan Necas
|
|
@@ -28,30 +28,36 @@ dependencies:
|
|
|
28
28
|
name: concurrent-ruby
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
|
-
- - "
|
|
31
|
+
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
33
|
version: 1.1.3
|
|
34
|
+
- - "<"
|
|
35
|
+
- !ruby/object:Gem::Version
|
|
36
|
+
version: '2.0'
|
|
34
37
|
type: :runtime
|
|
35
38
|
prerelease: false
|
|
36
39
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
40
|
requirements:
|
|
38
|
-
- - "
|
|
41
|
+
- - ">="
|
|
39
42
|
- !ruby/object:Gem::Version
|
|
40
43
|
version: 1.1.3
|
|
44
|
+
- - "<"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.0'
|
|
41
47
|
- !ruby/object:Gem::Dependency
|
|
42
48
|
name: concurrent-ruby-edge
|
|
43
49
|
requirement: !ruby/object:Gem::Requirement
|
|
44
50
|
requirements:
|
|
45
51
|
- - "~>"
|
|
46
52
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: 0.
|
|
53
|
+
version: 0.7.0
|
|
48
54
|
type: :runtime
|
|
49
55
|
prerelease: false
|
|
50
56
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
57
|
requirements:
|
|
52
58
|
- - "~>"
|
|
53
59
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: 0.
|
|
60
|
+
version: 0.7.0
|
|
55
61
|
- !ruby/object:Gem::Dependency
|
|
56
62
|
name: csv
|
|
57
63
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -66,6 +72,20 @@ dependencies:
|
|
|
66
72
|
- - "~>"
|
|
67
73
|
- !ruby/object:Gem::Version
|
|
68
74
|
version: '3.1'
|
|
75
|
+
- !ruby/object:Gem::Dependency
|
|
76
|
+
name: logger
|
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
type: :runtime
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
69
89
|
- !ruby/object:Gem::Dependency
|
|
70
90
|
name: msgpack
|
|
71
91
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -745,7 +765,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
745
765
|
- !ruby/object:Gem::Version
|
|
746
766
|
version: '0'
|
|
747
767
|
requirements: []
|
|
748
|
-
rubygems_version: 4.0.
|
|
768
|
+
rubygems_version: 4.0.20
|
|
749
769
|
specification_version: 4
|
|
750
770
|
summary: DYNamic workFLOW engine
|
|
751
771
|
test_files: []
|