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