dynflow 0.8.30 → 0.8.31
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/examples/singletons.rb +53 -0
- data/lib/dynflow/action.rb +15 -0
- data/lib/dynflow/action/singleton.rb +43 -0
- data/lib/dynflow/coordinator.rb +22 -0
- data/lib/dynflow/dispatcher.rb +8 -2
- data/lib/dynflow/dispatcher/client_dispatcher.rb +4 -1
- data/lib/dynflow/dispatcher/executor_dispatcher.rb +7 -1
- data/lib/dynflow/execution_plan.rb +12 -0
- data/lib/dynflow/executors/abstract.rb +4 -0
- data/lib/dynflow/executors/parallel.rb +4 -0
- data/lib/dynflow/executors/parallel/core.rb +4 -0
- data/lib/dynflow/executors/parallel/pool.rb +17 -0
- data/lib/dynflow/middleware.rb +1 -0
- data/lib/dynflow/middleware/common/singleton.rb +19 -0
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/web/console.rb +10 -0
- data/lib/dynflow/world.rb +6 -0
- data/test/action_test.rb +141 -0
- data/test/execution_plan_test.rb +36 -0
- data/test/executor_test.rb +7 -0
- data/test/world_test.rb +13 -0
- data/web/views/worlds.erb +5 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e4b6b017069b69e85804fc7f3b3256522835a536
|
4
|
+
data.tar.gz: c63330ee0e91061d41ba967f923e8eff2d094288
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f7cc6ca73e1f6157745476f3d2877fa80a0cfccd46b2597eff68ec2eb5933dbfebcf051684ffdc77dc6f09aeb30a82846913ebdb3bdec9b1853b23df0760cdff
|
7
|
+
data.tar.gz: 818a9763732ec3ca9baedb652610c1111054756cd32f1f2eae22a9a0ce6946651c364964a8632dd19e5c50d6265466013002ea34b704b7f17a48c40330bab4a2
|
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
example_description = <<DESC
|
3
|
+
|
4
|
+
Sub Plans Example
|
5
|
+
===================
|
6
|
+
|
7
|
+
This example shows, how singleton actions can be used for making sure
|
8
|
+
there is only one instance of the action running at a time.
|
9
|
+
|
10
|
+
Singleton actions try to obtain a lock at the beggining of their plan
|
11
|
+
phase and fail if they can't do so. In run phase they check if they
|
12
|
+
have the lock and try to acquire it again if they don't. These actions
|
13
|
+
release the lock at the end of their finalize phase.
|
14
|
+
|
15
|
+
DESC
|
16
|
+
|
17
|
+
require_relative 'example_helper'
|
18
|
+
|
19
|
+
class SingletonExample < Dynflow::Action
|
20
|
+
include Dynflow::Action::Singleton
|
21
|
+
|
22
|
+
def run
|
23
|
+
sleep 10
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class SingletonExampleA < SingletonExample; end
|
28
|
+
class SingletonExampleB < SingletonExample; end
|
29
|
+
|
30
|
+
if $0 == __FILE__
|
31
|
+
ExampleHelper.world.action_logger.level = Logger::INFO
|
32
|
+
ExampleHelper.world
|
33
|
+
t1 = ExampleHelper.world.trigger(SingletonExampleA)
|
34
|
+
t2 = ExampleHelper.world.trigger(SingletonExampleA)
|
35
|
+
ExampleHelper.world.trigger(SingletonExampleA) unless SingletonExampleA.singleton_locked?(ExampleHelper.world)
|
36
|
+
t3 = ExampleHelper.world.trigger(SingletonExampleB)
|
37
|
+
db = ExampleHelper.world.persistence.adapter.db
|
38
|
+
|
39
|
+
puts example_description
|
40
|
+
puts <<-MSG.gsub(/^.*\|/, '')
|
41
|
+
| 3 execution plans were triggered:
|
42
|
+
| #{t1.id} should finish successfully
|
43
|
+
| #{t3.id} should finish successfully because it is a singleton of different class
|
44
|
+
| #{t2.id} should fail because #{t1.id} holds the lock
|
45
|
+
|
|
46
|
+
| You can see the details at
|
47
|
+
| http://localhost:4567/#{t1.id}
|
48
|
+
| http://localhost:4567/#{t2.id}
|
49
|
+
| http://localhost:4567/#{t3.id}
|
50
|
+
|
|
51
|
+
MSG
|
52
|
+
ExampleHelper.run_web_console
|
53
|
+
end
|
data/lib/dynflow/action.rb
CHANGED
@@ -21,6 +21,7 @@ module Dynflow
|
|
21
21
|
|
22
22
|
require 'dynflow/action/polling'
|
23
23
|
require 'dynflow/action/cancellable'
|
24
|
+
require 'dynflow/action/singleton'
|
24
25
|
require 'dynflow/action/with_sub_plans'
|
25
26
|
require 'dynflow/action/with_bulk_sub_plans'
|
26
27
|
require 'dynflow/action/with_polling_sub_plans'
|
@@ -302,6 +303,10 @@ module Dynflow
|
|
302
303
|
@serializer
|
303
304
|
end
|
304
305
|
|
306
|
+
def holds_singleton_lock?
|
307
|
+
false
|
308
|
+
end
|
309
|
+
|
305
310
|
protected
|
306
311
|
|
307
312
|
def state=(state)
|
@@ -549,6 +554,16 @@ module Dynflow
|
|
549
554
|
def root_action?
|
550
555
|
@triggering_action.nil?
|
551
556
|
end
|
557
|
+
|
558
|
+
# An action must be a singleton and have a singleton lock
|
559
|
+
def self.singleton_locked?(world)
|
560
|
+
if self.ancestors.include? ::Dynflow::Action::Singleton
|
561
|
+
lock_class = ::Dynflow::Coordinator::SingletonActionLock
|
562
|
+
world.coordinator.find_locks(lock_class.unique_filter(self.name)).any?
|
563
|
+
else
|
564
|
+
false
|
565
|
+
end
|
566
|
+
end
|
552
567
|
end
|
553
568
|
# rubocop:enable Metrics/ClassLength
|
554
569
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Dynflow
|
2
|
+
class Action
|
3
|
+
module Singleton
|
4
|
+
def self.included(base)
|
5
|
+
base.middleware.use ::Dynflow::Middleware::Common::Singleton
|
6
|
+
end
|
7
|
+
|
8
|
+
def validate_singleton_lock!
|
9
|
+
singleton_lock! unless holds_singleton_lock?
|
10
|
+
end
|
11
|
+
|
12
|
+
def singleton_lock!
|
13
|
+
world.coordinator.acquire(singleton_lock)
|
14
|
+
rescue Dynflow::Coordinator::LockError
|
15
|
+
fail "Action #{self.class.name} is already active"
|
16
|
+
end
|
17
|
+
|
18
|
+
def singleton_unlock!
|
19
|
+
world.coordinator.release(singleton_lock) if holds_singleton_lock?
|
20
|
+
end
|
21
|
+
|
22
|
+
def holds_singleton_lock?
|
23
|
+
# Get locks for this action, there should be none or one
|
24
|
+
lock_filter = singleton_lock_class.unique_filter(self.class.name)
|
25
|
+
present_locks = world.coordinator.find_locks lock_filter
|
26
|
+
!present_locks.empty? && present_locks.first.owner_id == execution_plan_id
|
27
|
+
end
|
28
|
+
|
29
|
+
def singleton_lock_class
|
30
|
+
::Dynflow::Coordinator::SingletonActionLock
|
31
|
+
end
|
32
|
+
|
33
|
+
def singleton_lock
|
34
|
+
singleton_lock_class.new(self.class.name, execution_plan_id)
|
35
|
+
end
|
36
|
+
|
37
|
+
def error!(*args)
|
38
|
+
singleton_unlock!
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/dynflow/coordinator.rb
CHANGED
@@ -220,6 +220,28 @@ module Dynflow
|
|
220
220
|
end
|
221
221
|
end
|
222
222
|
|
223
|
+
# Used when there should be only one execution plan for a given action class
|
224
|
+
class SingletonActionLock < Lock
|
225
|
+
def initialize(action_class, execution_plan_id)
|
226
|
+
super
|
227
|
+
@data[:owner_id] = "execution-plan:#{execution_plan_id}"
|
228
|
+
@data[:execution_plan_id] = execution_plan_id
|
229
|
+
@data[:id] = self.class.lock_id(action_class)
|
230
|
+
end
|
231
|
+
|
232
|
+
def owner_id
|
233
|
+
@data[:execution_plan_id]
|
234
|
+
end
|
235
|
+
|
236
|
+
def self.unique_filter(action_class)
|
237
|
+
{ :class => self.name, :id => self.lock_id(action_class) }
|
238
|
+
end
|
239
|
+
|
240
|
+
def self.lock_id(action_class)
|
241
|
+
'singleton-action:' + action_class
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
223
245
|
class ExecutionLock < LockByWorld
|
224
246
|
def initialize(world, execution_plan_id, client_world_id, request_id)
|
225
247
|
super(world)
|
data/lib/dynflow/dispatcher.rb
CHANGED
@@ -15,14 +15,20 @@ module Dynflow
|
|
15
15
|
fields! receiver_id: String
|
16
16
|
end
|
17
17
|
|
18
|
-
|
18
|
+
Status = type do
|
19
|
+
fields! receiver_id: String,
|
20
|
+
execution_plan_id: type { variants String, NilClass }
|
21
|
+
end
|
22
|
+
|
23
|
+
variants Event, Execution, Ping, Status
|
19
24
|
end
|
20
25
|
|
21
26
|
Response = Algebrick.type do
|
22
27
|
variants Accepted = atom,
|
23
28
|
Failed = type { fields! error: String },
|
24
29
|
Done = atom,
|
25
|
-
Pong = atom
|
30
|
+
Pong = atom,
|
31
|
+
ExecutionStatus = type { fields! execution_status: Hash }
|
26
32
|
end
|
27
33
|
|
28
34
|
Envelope = Algebrick.type do
|
@@ -58,7 +58,7 @@ module Dynflow
|
|
58
58
|
(on ~Event do |event|
|
59
59
|
find_executor(event.execution_plan_id)
|
60
60
|
end),
|
61
|
-
(on Ping.(~any) do |receiver_id|
|
61
|
+
(on Ping.(~any) | Status.(~any, ~any) do |receiver_id, _|
|
62
62
|
receiver_id
|
63
63
|
end)
|
64
64
|
envelope = Envelope[request_id, client_world_id, executor_id, request]
|
@@ -82,6 +82,9 @@ module Dynflow
|
|
82
82
|
end),
|
83
83
|
(on Done | Pong do
|
84
84
|
resolve_tracked_request(envelope.request_id)
|
85
|
+
end),
|
86
|
+
(on ExecutionStatus.(~any) do |steps|
|
87
|
+
@tracked_requests.delete(envelope.request_id).success! steps
|
85
88
|
end)
|
86
89
|
end
|
87
90
|
|
@@ -9,7 +9,8 @@ module Dynflow
|
|
9
9
|
def handle_request(envelope)
|
10
10
|
match(envelope.message,
|
11
11
|
on(Execution) { perform_execution(envelope, envelope.message) },
|
12
|
-
on(Event) { perform_event(envelope, envelope.message) }
|
12
|
+
on(Event) { perform_event(envelope, envelope.message) },
|
13
|
+
on(Status) { get_execution_status(envelope, envelope.message) })
|
13
14
|
end
|
14
15
|
|
15
16
|
protected
|
@@ -63,6 +64,11 @@ module Dynflow
|
|
63
64
|
end
|
64
65
|
end
|
65
66
|
|
67
|
+
def get_execution_status(envelope, envelope_message)
|
68
|
+
items = @world.executor.execution_status envelope_message.execution_plan_id
|
69
|
+
respond(envelope, ExecutionStatus[execution_status: items])
|
70
|
+
end
|
71
|
+
|
66
72
|
private
|
67
73
|
|
68
74
|
def allocate_executor(execution_plan_id, client_world_id, request_id)
|
@@ -117,6 +117,9 @@ module Dynflow
|
|
117
117
|
@ended_at = Time.now
|
118
118
|
@real_time = @ended_at - @started_at unless @started_at.nil?
|
119
119
|
@execution_time = compute_execution_time
|
120
|
+
unlock_all_singleton_locks!
|
121
|
+
when :paused
|
122
|
+
unlock_all_singleton_locks!
|
120
123
|
else
|
121
124
|
# ignore
|
122
125
|
end
|
@@ -254,6 +257,7 @@ module Dynflow
|
|
254
257
|
if @run_flow.size == 1
|
255
258
|
@run_flow = @run_flow.sub_flows.first
|
256
259
|
end
|
260
|
+
|
257
261
|
steps.values.each(&:save)
|
258
262
|
update_state(error? ? :stopped : :planned)
|
259
263
|
end
|
@@ -505,6 +509,14 @@ module Dynflow
|
|
505
509
|
end
|
506
510
|
end
|
507
511
|
|
512
|
+
def unlock_all_singleton_locks!
|
513
|
+
filter = { :owner_id => 'execution-plan:' + self.id,
|
514
|
+
:class => Dynflow::Coordinator::SingletonActionLock.to_s }
|
515
|
+
world.coordinator.find_locks(filter).each do |lock|
|
516
|
+
world.coordinator.release(lock)
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
508
520
|
private_class_method :steps_from_hash
|
509
521
|
end
|
510
522
|
end
|
@@ -23,6 +23,17 @@ module Dynflow
|
|
23
23
|
@jobs.empty?
|
24
24
|
end
|
25
25
|
|
26
|
+
def execution_status(execution_plan_id = nil)
|
27
|
+
source = if execution_plan_id.nil?
|
28
|
+
@jobs
|
29
|
+
else
|
30
|
+
{ execution_plan_id => @jobs.fetch(execution_plan_id, []) }
|
31
|
+
end
|
32
|
+
source.reduce({}) do |acc, (plan_id, work_items)|
|
33
|
+
acc.update(plan_id => work_items.count)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
26
37
|
private
|
27
38
|
|
28
39
|
def tracked?(work)
|
@@ -62,6 +73,12 @@ module Dynflow
|
|
62
73
|
try_to_terminate
|
63
74
|
end
|
64
75
|
|
76
|
+
def execution_status(execution_plan_id = nil)
|
77
|
+
{ :pool_size => @pool_size,
|
78
|
+
:free_workers => @free_workers.count,
|
79
|
+
:execution_status => @jobs.execution_status(execution_plan_id) }
|
80
|
+
end
|
81
|
+
|
65
82
|
private
|
66
83
|
|
67
84
|
def try_to_terminate
|
data/lib/dynflow/middleware.rb
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Dynflow
|
2
|
+
module Middleware::Common
|
3
|
+
class Singleton < Middleware
|
4
|
+
# Each action tries to acquire its own lock before the action's #plan starts
|
5
|
+
def plan(*args)
|
6
|
+
action.singleton_lock!
|
7
|
+
pass(*args)
|
8
|
+
end
|
9
|
+
|
10
|
+
# At the start of #run we try to acquire action's lock unless it already holds it
|
11
|
+
# At the end the action tries to unlock its own lock if the execution plan has no
|
12
|
+
# finalize phase
|
13
|
+
def run(*args)
|
14
|
+
action.singleton_lock! unless action.holds_singleton_lock?
|
15
|
+
pass(*args)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/dynflow/version.rb
CHANGED
data/lib/dynflow/web/console.rb
CHANGED
@@ -37,6 +37,16 @@ module Dynflow
|
|
37
37
|
erb :worlds
|
38
38
|
end
|
39
39
|
|
40
|
+
post('/worlds/execution_status') do
|
41
|
+
@worlds = world.coordinator.find_worlds(true)
|
42
|
+
@worlds.each do |w|
|
43
|
+
hash = world.get_execution_status(w.data['id'], nil, 5).value!
|
44
|
+
hash[:execution_status] = hash[:execution_status].values.reduce(:+) || 0
|
45
|
+
w.data.update(hash)
|
46
|
+
end
|
47
|
+
erb :worlds
|
48
|
+
end
|
49
|
+
|
40
50
|
post('/worlds/check') do
|
41
51
|
@worlds = world.coordinator.find_worlds
|
42
52
|
@validation_results = world.worlds_validity_check(params[:invalidate])
|
data/lib/dynflow/world.rb
CHANGED
@@ -198,6 +198,10 @@ module Dynflow
|
|
198
198
|
publish_request(Dispatcher::Ping[world_id], done, false, timeout)
|
199
199
|
end
|
200
200
|
|
201
|
+
def get_execution_status(world_id, execution_plan_id, timeout, done = Concurrent.future)
|
202
|
+
publish_request(Dispatcher::Status[world_id, execution_plan_id], done, false, timeout)
|
203
|
+
end
|
204
|
+
|
201
205
|
def publish_request(request, done, wait_for_accepted, timeout = nil)
|
202
206
|
accepted = Concurrent.future
|
203
207
|
accepted.rescue do |reason|
|
@@ -299,11 +303,13 @@ module Dynflow
|
|
299
303
|
logger.error "unexpected error when invalidating execution plan #{execution_lock.execution_plan_id}, skipping"
|
300
304
|
end
|
301
305
|
coordinator.release(execution_lock)
|
306
|
+
coordinator.release_by_owner(execution_lock.execution_plan_id)
|
302
307
|
return
|
303
308
|
end
|
304
309
|
unless plan.valid?
|
305
310
|
logger.error "invalid plan #{plan.id}, skipping"
|
306
311
|
coordinator.release(execution_lock)
|
312
|
+
coordinator.release_by_owner(execution_lock.execution_plan_id)
|
307
313
|
return
|
308
314
|
end
|
309
315
|
plan.execution_history.add('terminate execution', execution_lock.world_id)
|
data/test/action_test.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative 'test_helper'
|
2
|
+
require 'mocha/mini_test'
|
2
3
|
|
3
4
|
module Dynflow
|
4
5
|
describe 'action' do
|
@@ -671,6 +672,146 @@ module Dynflow
|
|
671
672
|
clock.pending_pings.count.must_equal 0
|
672
673
|
end
|
673
674
|
end
|
675
|
+
|
676
|
+
describe ::Dynflow::Action::Singleton do
|
677
|
+
include TestHelpers
|
678
|
+
|
679
|
+
let(:clock) { Dynflow::Testing::ManagedClock.new }
|
680
|
+
|
681
|
+
class SingletonAction < ::Dynflow::Action
|
682
|
+
include ::Dynflow::Action::Singleton
|
683
|
+
end
|
684
|
+
|
685
|
+
class SingletonActionWithRun < SingletonAction
|
686
|
+
def run; end
|
687
|
+
end
|
688
|
+
|
689
|
+
class SingletonActionWithFinalize < SingletonAction
|
690
|
+
def finalize; end
|
691
|
+
end
|
692
|
+
|
693
|
+
class SuspendedSingletonAction < SingletonAction
|
694
|
+
def run(event = nil)
|
695
|
+
unless output[:suspended]
|
696
|
+
output[:suspended] = true
|
697
|
+
suspend
|
698
|
+
end
|
699
|
+
end
|
700
|
+
end
|
701
|
+
|
702
|
+
class BadAction < SingletonAction
|
703
|
+
def plan(break_locks = false)
|
704
|
+
plan_self :break_locks => break_locks
|
705
|
+
singleton_unlock! if break_locks
|
706
|
+
end
|
707
|
+
|
708
|
+
def run
|
709
|
+
singleton_unlock! if input[:break_locks]
|
710
|
+
end
|
711
|
+
end
|
712
|
+
|
713
|
+
it 'unlocks the locks after #plan if no #run or #finalize' do
|
714
|
+
plan = world.plan(SingletonAction)
|
715
|
+
plan.state.must_equal :planned
|
716
|
+
lock_filter = ::Dynflow::Coordinator::SingletonActionLock
|
717
|
+
.unique_filter plan.entry_action.class.name
|
718
|
+
world.coordinator.find_locks(lock_filter).count.must_equal 1
|
719
|
+
plan = world.execute(plan.id).wait!.value
|
720
|
+
plan.state.must_equal :stopped
|
721
|
+
plan.result.must_equal :success
|
722
|
+
world.coordinator.find_locks(lock_filter).count.must_equal 0
|
723
|
+
end
|
724
|
+
|
725
|
+
it 'unlocks the locks after #finalize' do
|
726
|
+
plan = world.plan(SingletonActionWithFinalize)
|
727
|
+
plan.state.must_equal :planned
|
728
|
+
lock_filter = ::Dynflow::Coordinator::SingletonActionLock
|
729
|
+
.unique_filter plan.entry_action.class.name
|
730
|
+
world.coordinator.find_locks(lock_filter).count.must_equal 1
|
731
|
+
plan = world.execute(plan.id).wait!.value
|
732
|
+
plan.state.must_equal :stopped
|
733
|
+
plan.result.must_equal :success
|
734
|
+
world.coordinator.find_locks(lock_filter).count.must_equal 0
|
735
|
+
end
|
736
|
+
|
737
|
+
it 'does not unlock when getting suspended' do
|
738
|
+
plan = world.plan(SuspendedSingletonAction)
|
739
|
+
plan.state.must_equal :planned
|
740
|
+
lock_filter = ::Dynflow::Coordinator::SingletonActionLock
|
741
|
+
.unique_filter plan.entry_action.class.name
|
742
|
+
world.coordinator.find_locks(lock_filter).count.must_equal 1
|
743
|
+
future = world.execute(plan.id)
|
744
|
+
wait_for do
|
745
|
+
plan = world.persistence.load_execution_plan(plan.id)
|
746
|
+
plan.state == :running && plan.result == :pending
|
747
|
+
end
|
748
|
+
world.coordinator.find_locks(lock_filter).count.must_equal 1
|
749
|
+
world.event(plan.id, 2, nil)
|
750
|
+
plan = future.wait!.value
|
751
|
+
plan.state.must_equal :stopped
|
752
|
+
plan.result.must_equal :success
|
753
|
+
world.coordinator.find_locks(lock_filter).count.must_equal 0
|
754
|
+
end
|
755
|
+
|
756
|
+
it 'can be triggered only once' do
|
757
|
+
# plan1 acquires the lock in plan phase
|
758
|
+
plan1 = world.plan(SingletonActionWithRun)
|
759
|
+
plan1.state.must_equal :planned
|
760
|
+
plan1.result.must_equal :pending
|
761
|
+
|
762
|
+
# plan2 tries to acquire the lock in plan phase and fails
|
763
|
+
plan2 = world.plan(SingletonActionWithRun)
|
764
|
+
plan2.state.must_equal :stopped
|
765
|
+
plan2.result.must_equal :error
|
766
|
+
plan2.errors.first.message.must_equal 'Action Dynflow::SingletonActionWithRun is already active'
|
767
|
+
|
768
|
+
# Simulate some bad things happening
|
769
|
+
plan1.entry_action.send(:singleton_unlock!)
|
770
|
+
|
771
|
+
# plan3 acquires the lock in plan phase
|
772
|
+
plan3 = world.plan(SingletonActionWithRun)
|
773
|
+
|
774
|
+
# plan1 tries to relock on run
|
775
|
+
# This should fail because the lock was taken by plan3
|
776
|
+
plan1 = world.execute(plan1.id).wait!.value
|
777
|
+
plan1.state.must_equal :paused
|
778
|
+
plan1.result.must_equal :error
|
779
|
+
|
780
|
+
# plan3 can finish successfully because it holds the lock
|
781
|
+
plan3 = world.execute(plan3.id).wait!.value
|
782
|
+
plan3.state.must_equal :stopped
|
783
|
+
plan3.result.must_equal :success
|
784
|
+
|
785
|
+
# The lock was released when plan3 stopped
|
786
|
+
lock_filter = ::Dynflow::Coordinator::SingletonActionLock
|
787
|
+
.unique_filter plan3.entry_action.class.name
|
788
|
+
world.coordinator.find_locks(lock_filter).must_be :empty?
|
789
|
+
end
|
790
|
+
|
791
|
+
it 'cannot be unlocked by another action' do
|
792
|
+
# plan1 doesn't keep its locks
|
793
|
+
plan1 = world.plan(BadAction, true)
|
794
|
+
plan1.state.must_equal :planned
|
795
|
+
lock_filter = ::Dynflow::Coordinator::SingletonActionLock
|
796
|
+
.unique_filter plan1.entry_action.class.name
|
797
|
+
world.coordinator.find_locks(lock_filter).count.must_equal 0
|
798
|
+
plan2 = world.plan(BadAction, false)
|
799
|
+
plan2.state.must_equal :planned
|
800
|
+
world.coordinator.find_locks(lock_filter).count.must_equal 1
|
801
|
+
|
802
|
+
# The locks held by plan2 can't be unlocked by plan1
|
803
|
+
plan1.entry_action.singleton_unlock!
|
804
|
+
world.coordinator.find_locks(lock_filter).count.must_equal 1
|
805
|
+
|
806
|
+
plan1 = world.execute(plan1.id).wait!.value
|
807
|
+
plan1.state.must_equal :paused
|
808
|
+
plan1.result.must_equal :error
|
809
|
+
|
810
|
+
plan2 = world.execute(plan2.id).wait!.value
|
811
|
+
plan2.state.must_equal :stopped
|
812
|
+
plan2.result.must_equal :success
|
813
|
+
end
|
814
|
+
end
|
674
815
|
end
|
675
816
|
end
|
676
817
|
end
|
data/test/execution_plan_test.rb
CHANGED
@@ -341,6 +341,42 @@ module Dynflow
|
|
341
341
|
end
|
342
342
|
|
343
343
|
end
|
344
|
+
|
345
|
+
describe 'with singleton actions' do
|
346
|
+
class SingletonAction < ::Dynflow::Action
|
347
|
+
include ::Dynflow::Action::Singleton
|
348
|
+
|
349
|
+
def run
|
350
|
+
if input[:fail]
|
351
|
+
raise "Controlled Failure"
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
it 'unlocks the locks on transition to stopped' do
|
357
|
+
plan = world.plan(SingletonAction)
|
358
|
+
plan.state.must_equal :planned
|
359
|
+
lock_filter = ::Dynflow::Coordinator::SingletonActionLock
|
360
|
+
.unique_filter plan.entry_action.class.name
|
361
|
+
world.coordinator.find_locks(lock_filter).count.must_equal 1
|
362
|
+
plan = world.execute(plan.id).wait!.value
|
363
|
+
plan.state.must_equal :stopped
|
364
|
+
plan.result.must_equal :success
|
365
|
+
world.coordinator.find_locks(lock_filter).count.must_equal 0
|
366
|
+
end
|
367
|
+
|
368
|
+
it 'unlocks the locks on transition to paused' do
|
369
|
+
plan = world.plan(SingletonAction, :fail => true)
|
370
|
+
plan.state.must_equal :planned
|
371
|
+
lock_filter = ::Dynflow::Coordinator::SingletonActionLock
|
372
|
+
.unique_filter plan.entry_action.class.name
|
373
|
+
world.coordinator.find_locks(lock_filter).count.must_equal 1
|
374
|
+
plan = world.execute(plan.id).wait!.value
|
375
|
+
plan.state.must_equal :paused
|
376
|
+
plan.result.must_equal :error
|
377
|
+
world.coordinator.find_locks(lock_filter).count.must_equal 0
|
378
|
+
end
|
379
|
+
end
|
344
380
|
end
|
345
381
|
end
|
346
382
|
end
|
data/test/executor_test.rb
CHANGED
@@ -580,10 +580,12 @@ module Dynflow
|
|
580
580
|
let(:storage) { Dynflow::Executors::Parallel::Pool::JobStorage.new }
|
581
581
|
it do
|
582
582
|
storage.must_be_empty
|
583
|
+
storage.execution_status.must_equal({})
|
583
584
|
storage.pop.must_be_nil
|
584
585
|
storage.pop.must_be_nil
|
585
586
|
|
586
587
|
storage.add s = FakeStep.new(1)
|
588
|
+
storage.execution_status.must_equal(1 => 1)
|
587
589
|
storage.pop.must_equal s
|
588
590
|
storage.must_be_empty
|
589
591
|
storage.pop.must_be_nil
|
@@ -595,6 +597,10 @@ module Dynflow
|
|
595
597
|
storage.add s22 = FakeStep.new(2)
|
596
598
|
storage.add s31 = FakeStep.new(3)
|
597
599
|
|
600
|
+
storage.execution_status(1).must_equal(1 => 3)
|
601
|
+
storage.execution_status(4).must_equal(4 => 0)
|
602
|
+
storage.execution_status.must_equal({1 => 3, 2 => 2, 3 => 1})
|
603
|
+
|
598
604
|
storage.pop.must_equal s21
|
599
605
|
storage.pop.must_equal s31
|
600
606
|
storage.pop.must_equal s11
|
@@ -603,6 +609,7 @@ module Dynflow
|
|
603
609
|
storage.pop.must_equal s13
|
604
610
|
|
605
611
|
storage.must_be_empty
|
612
|
+
storage.execution_status.must_equal({})
|
606
613
|
storage.pop.must_be_nil
|
607
614
|
end
|
608
615
|
end
|
data/test/world_test.rb
CHANGED
@@ -19,6 +19,19 @@ module Dynflow
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
+
describe '#get_execution_status' do
|
23
|
+
let(:base) do
|
24
|
+
{ :pool_size => 5, :free_workers => 5, :execution_status => {} }
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'retrieves correct execution items count' do
|
28
|
+
world.get_execution_status(world.id, nil, 5).value!.must_equal(base)
|
29
|
+
id = 'something like uuid'
|
30
|
+
expected = base.merge(:execution_status => { id => 0 })
|
31
|
+
world.get_execution_status(world.id, id, 5).value!.must_equal(expected)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
22
35
|
describe '#terminate' do
|
23
36
|
it 'fires an event after termination' do
|
24
37
|
terminated_event = world.terminated
|
data/web/views/worlds.erb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
<ul>
|
4
4
|
<li><a href="<%= url("/worlds/check") %>" class="postlink">check status</a>: see potentially invalid worlds</li>
|
5
5
|
<li><a href="<%= url("/worlds/check?invalidate=true") %>" class="postlink">check and invalidate</a>: invalidate the worlds that don't respond</li>
|
6
|
+
<li><a href="<%= url("/worlds/execution_status") %>" class="postlink">load execution items counts</a>: see counts of execution items per world</li>
|
6
7
|
</ul>
|
7
8
|
|
8
9
|
<table class="table">
|
@@ -11,6 +12,8 @@
|
|
11
12
|
<th>Id</th>
|
12
13
|
<th>Meta</th>
|
13
14
|
<th>Executor?</th>
|
15
|
+
<th>Execution items</th>
|
16
|
+
<th>Free/Total workers</th>
|
14
17
|
<th></th>
|
15
18
|
</tr>
|
16
19
|
</thead>
|
@@ -20,6 +23,8 @@
|
|
20
23
|
<td><%= h(world.id) %></td>
|
21
24
|
<td><%= h(world.meta) %></td>
|
22
25
|
<td><%= "true" if world.is_a? Dynflow::Coordinator::ExecutorWorld %></td>
|
26
|
+
<td><%= h(world.data['execution_status'] || 'N/A') %></td>
|
27
|
+
<td><%= world.data.key?('free_workers') ? "#{world.data['free_workers']}/#{world.data[:pool_size]}" : 'N/A' %></td>
|
23
28
|
<td>
|
24
29
|
<% validation_result = @validation_results[world.id] if @validation_results %>
|
25
30
|
|
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: 0.8.
|
4
|
+
version: 0.8.31
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ivan Necas
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2017-
|
12
|
+
date: 2017-10-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: multi_json
|
@@ -377,6 +377,7 @@ files:
|
|
377
377
|
- examples/orchestrate.rb
|
378
378
|
- examples/orchestrate_evented.rb
|
379
379
|
- examples/remote_executor.rb
|
380
|
+
- examples/singletons.rb
|
380
381
|
- examples/sub_plan_concurrency_control.rb
|
381
382
|
- examples/sub_plans.rb
|
382
383
|
- lib/dynflow.rb
|
@@ -387,6 +388,7 @@ files:
|
|
387
388
|
- lib/dynflow/action/polling.rb
|
388
389
|
- lib/dynflow/action/progress.rb
|
389
390
|
- lib/dynflow/action/rescue.rb
|
391
|
+
- lib/dynflow/action/singleton.rb
|
390
392
|
- lib/dynflow/action/suspended.rb
|
391
393
|
- lib/dynflow/action/timeouts.rb
|
392
394
|
- lib/dynflow/action/with_bulk_sub_plans.rb
|
@@ -455,6 +457,7 @@ files:
|
|
455
457
|
- lib/dynflow/logger_adapters/formatters/exception.rb
|
456
458
|
- lib/dynflow/logger_adapters/simple.rb
|
457
459
|
- lib/dynflow/middleware.rb
|
460
|
+
- lib/dynflow/middleware/common/singleton.rb
|
458
461
|
- lib/dynflow/middleware/common/transaction.rb
|
459
462
|
- lib/dynflow/middleware/register.rb
|
460
463
|
- lib/dynflow/middleware/resolver.rb
|