dynflow 0.8.30 → 0.8.31

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d71060f0924aa889e0426c476d6b7cf9366475d3
4
- data.tar.gz: 6008428d10eff1655d8554526e139a46ec2b68c4
3
+ metadata.gz: e4b6b017069b69e85804fc7f3b3256522835a536
4
+ data.tar.gz: c63330ee0e91061d41ba967f923e8eff2d094288
5
5
  SHA512:
6
- metadata.gz: bd3c82ba611cb82460817d6c5e3ac985a859b4dfb6499ab11508209e1d87605f4dee57340ac67825fb4ca78c7f49d00b66b71f2d7137d83292595fc4110f42ac
7
- data.tar.gz: 5fda41ada7581f1601f407cfe093ed6f3e4d902923fdf27dbe6dbcc73a69f18ffcfd5db4afd105d88537e90e7ee38bd546eaed8caa40609b3ce184bb067ab590
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
@@ -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
@@ -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)
@@ -15,14 +15,20 @@ module Dynflow
15
15
  fields! receiver_id: String
16
16
  end
17
17
 
18
- variants Event, Execution, Ping
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
@@ -27,6 +27,10 @@ module Dynflow
27
27
  raise NotImplementedError
28
28
  end
29
29
 
30
+ def execution_status(execution_plan_id = nil)
31
+ raise NotImplementedError
32
+ end
33
+
30
34
  # @return [Concurrent::Edge::Future]
31
35
  def initialized
32
36
  raise NotImplementedError
@@ -35,6 +35,10 @@ module Dynflow
35
35
  future
36
36
  end
37
37
 
38
+ def execution_status(execution_plan_id = nil)
39
+ @core.ask!([:execution_status, execution_plan_id])
40
+ end
41
+
38
42
  def initialized
39
43
  @core_initialized
40
44
  end
@@ -57,6 +57,10 @@ module Dynflow
57
57
  @world.dead_letter_handler
58
58
  end
59
59
 
60
+ def execution_status(execution_plan_id = nil)
61
+ @pool.ask!([:execution_status, execution_plan_id])
62
+ end
63
+
60
64
  private
61
65
 
62
66
  def on_message(message)
@@ -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
@@ -5,6 +5,7 @@ module Dynflow
5
5
  require 'dynflow/middleware/resolver'
6
6
  require 'dynflow/middleware/stack'
7
7
  require 'dynflow/middleware/common/transaction'
8
+ require 'dynflow/middleware/common/singleton'
8
9
 
9
10
  include Algebrick::TypeCheck
10
11
 
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Dynflow
2
- VERSION = '0.8.30'
2
+ VERSION = '0.8.31'
3
3
  end
@@ -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
@@ -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
@@ -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.30
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-09-15 00:00:00.000000000 Z
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