From 410de4ed72b41cccba4d8de6e371b099bf5e4520 Mon Sep 17 00:00:00 2001 From: Adam Ruzicka Date: Fri, 17 May 2024 13:36:27 +0200 Subject: [PATCH] Add execution plan chaining This commit enables execution plans to be chained. Assuming there is an execution plan EP1, another execution plan EP2 can be chained onto EP1. When chained, EP2 will stay in scheduled state until EP1 goes to stopped state. An execution plan can be chained onto multiple prerequisite execution plans, in which case it will be run once all the prerequisite execution plans are stopped and have not failed. If the prerequisite execution plan ends with stopped-error, the chained execution plan(s) will fail. If the prerequisite execution plan is halted, the chained execution plan(s) will be run. It builds on mechanisms which were already present. When an execution plan is chained, it behaves in the same way as if it was scheduled for future execution. A record is created in dynflow_delayed_table and once the conditions for it to execute are right, it is dispatched by the delayed executor. Because of this, there might be small delay between when the prerequisites finishs and the chained plan is started. --- examples/execution_plan_chaining.rb | 56 +++++++ lib/dynflow/debug/telemetry/persistence.rb | 2 +- .../delayed_executors/abstract_core.rb | 2 +- lib/dynflow/delayed_plan.rb | 6 + lib/dynflow/director.rb | 10 +- lib/dynflow/persistence.rb | 16 +- lib/dynflow/persistence_adapters/abstract.rb | 10 +- lib/dynflow/persistence_adapters/sequel.rb | 33 +++- .../025_create_execution_plan_dependencies.rb | 22 +++ lib/dynflow/world.rb | 10 ++ test/future_execution_test.rb | 152 +++++++++++++++++- test/persistence_test.rb | 73 ++++++++- test/support/dummy_example.rb | 4 + web/views/show.erb | 24 +++ 14 files changed, 404 insertions(+), 16 deletions(-) create mode 100755 examples/execution_plan_chaining.rb create mode 100644 lib/dynflow/persistence_adapters/sequel_migrations/025_create_execution_plan_dependencies.rb diff --git a/examples/execution_plan_chaining.rb b/examples/execution_plan_chaining.rb new file mode 100755 index 000000000..3468fca7f --- /dev/null +++ b/examples/execution_plan_chaining.rb @@ -0,0 +1,56 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative 'example_helper' + +class DelayedAction < Dynflow::Action + def plan(should_fail = false) + plan_self :should_fail => should_fail + end + + def run + sleep 5 + raise "Controlled failure" if input[:should_fail] + end + + def rescue_strategy + Dynflow::Action::Rescue::Fail + end +end + +if $PROGRAM_NAME == __FILE__ + world = ExampleHelper.create_world do |config| + config.auto_rescue = true + end + world.action_logger.level = 1 + world.logger.level = 0 + + plan1 = world.trigger(DelayedAction) + plan2 = world.chain(plan1.execution_plan_id, DelayedAction) + plan3 = world.chain(plan2.execution_plan_id, DelayedAction) + plan4 = world.chain(plan2.execution_plan_id, DelayedAction) + + plan5 = world.trigger(DelayedAction, true) + plan6 = world.chain(plan5.execution_plan_id, DelayedAction) + + puts <<-MSG.gsub(/^.*\|/, '') + | + | Execution Plan Chaining example + | ======================== + | + | This example shows the execution plan chaining functionality of Dynflow, which allows execution plans to wait until another execution plan finishes. + | + | Execution plans: + | #{plan1.id} runs immediately and should run successfully. + | #{plan2.id} is delayed and should run once #{plan1.id} finishes. + | #{plan3.id} and #{plan4.id} are delayed and should run once #{plan2.id} finishes. + | + | #{plan5.id} runs immediately and is expected to fail. + | #{plan6.id} should not run at all as its prerequisite failed. + | + | Visit #{ExampleHelper::DYNFLOW_URL} to see their status. + | + MSG + + ExampleHelper.run_web_console(world) +end diff --git a/lib/dynflow/debug/telemetry/persistence.rb b/lib/dynflow/debug/telemetry/persistence.rb index ad5cb38ca..4d6926112 100644 --- a/lib/dynflow/debug/telemetry/persistence.rb +++ b/lib/dynflow/debug/telemetry/persistence.rb @@ -19,7 +19,7 @@ module Persistence :load_execution_plan, :save_execution_plan, :find_old_execution_plans, - :find_past_delayed_plans, + :find_ready_delayed_plans, :delete_delayed_plans, :save_delayed_plan, :set_delayed_plan_frozen, diff --git a/lib/dynflow/delayed_executors/abstract_core.rb b/lib/dynflow/delayed_executors/abstract_core.rb index ec1d08e68..f9ec1833a 100644 --- a/lib/dynflow/delayed_executors/abstract_core.rb +++ b/lib/dynflow/delayed_executors/abstract_core.rb @@ -32,7 +32,7 @@ def time def delayed_execution_plans(time) with_error_handling([]) do - world.persistence.find_past_delayed_plans(time) + world.persistence.find_ready_delayed_plans(time) end end diff --git a/lib/dynflow/delayed_plan.rb b/lib/dynflow/delayed_plan.rb index 972ac8c52..5fd1c6e20 100644 --- a/lib/dynflow/delayed_plan.rb +++ b/lib/dynflow/delayed_plan.rb @@ -31,6 +31,12 @@ def timeout error("Execution plan could not be started before set time (#{@start_before})", 'timeout') end + def failed_dependencies(uuids) + bullets = uuids.map { |u| "- #{u}" }.join("\n") + msg = "Execution plan could not be started because some of its prerequisite execution plans failed:\n#{bullets}" + error(msg, 'failed-dependency') + end + def error(message, history_entry = nil) execution_plan.root_plan_step.state = :error execution_plan.root_plan_step.error = ::Dynflow::ExecutionPlan::Steps::Error.new(message) diff --git a/lib/dynflow/director.rb b/lib/dynflow/director.rb index 45b813cbe..14e497582 100644 --- a/lib/dynflow/director.rb +++ b/lib/dynflow/director.rb @@ -114,7 +114,15 @@ def execute plan = world.persistence.load_delayed_plan(execution_plan_id) return if plan.nil? || plan.execution_plan.state != :scheduled - if !plan.start_before.nil? && plan.start_before < Time.now.utc() + if plan.start_before.nil? + blocker_ids = world.persistence.find_execution_plan_dependencies(execution_plan_id) + statuses = world.persistence.find_execution_plan_statuses({ filters: { uuid: blocker_ids } }) + failed = statuses.select { |_uuid, status| status[:state] == 'stopped' && status[:result] == 'error' } + if failed.any? + plan.failed_dependencies(failed.keys) + return + end + elsif plan.start_before < Time.now.utc() plan.timeout return end diff --git a/lib/dynflow/persistence.rb b/lib/dynflow/persistence.rb index b5e16d043..3409004a5 100644 --- a/lib/dynflow/persistence.rb +++ b/lib/dynflow/persistence.rb @@ -101,8 +101,16 @@ def find_old_execution_plans(age) end end - def find_past_delayed_plans(time) - adapter.find_past_delayed_plans(time).map do |plan| + def find_execution_plan_dependencies(execution_plan_id) + adapter.find_execution_plan_dependencies(execution_plan_id) + end + + def find_blocked_execution_plans(execution_plan_id) + adapter.find_blocked_execution_plans(execution_plan_id) + end + + def find_ready_delayed_plans(time) + adapter.find_ready_delayed_plans(time).map do |plan| DelayedPlan.new_from_hash(@world, plan) end end @@ -163,5 +171,9 @@ def prune_envelopes(receiver_ids) def prune_undeliverable_envelopes adapter.prune_undeliverable_envelopes end + + def chain_execution_plan(first, second) + adapter.chain_execution_plan(first, second) + end end end diff --git a/lib/dynflow/persistence_adapters/abstract.rb b/lib/dynflow/persistence_adapters/abstract.rb index 994fb9b77..48a75e5ee 100644 --- a/lib/dynflow/persistence_adapters/abstract.rb +++ b/lib/dynflow/persistence_adapters/abstract.rb @@ -72,7 +72,15 @@ def save_execution_plan(execution_plan_id, value) raise NotImplementedError end - def find_past_delayed_plans(options = {}) + def find_execution_plan_dependencies(execution_plan_id) + raise NotImplementedError + end + + def find_blocked_execution_plans(execution_plan_id) + raise NotImplementedError + end + + def find_ready_delayed_plans(options = {}) raise NotImplementedError end diff --git a/lib/dynflow/persistence_adapters/sequel.rb b/lib/dynflow/persistence_adapters/sequel.rb index 2c321f859..409ce46bf 100644 --- a/lib/dynflow/persistence_adapters/sequel.rb +++ b/lib/dynflow/persistence_adapters/sequel.rb @@ -39,7 +39,8 @@ class action_class execution_plan_uuid queue), envelope: %w(receiver_id), coordinator_record: %w(id owner_id class), delayed: %w(execution_plan_uuid start_at start_before args_serializer frozen), - output_chunk: %w(execution_plan_uuid action_id kind timestamp) } + output_chunk: %w(execution_plan_uuid action_id kind timestamp), + execution_plan_dependency: %w(execution_plan_uuid blocked_by_uuid) } SERIALIZABLE_COLUMNS = { action: %w(input output), delayed: %w(serialized_args), @@ -153,12 +154,31 @@ def find_old_execution_plans(age) records.map { |plan| execution_plan_column_map(load_data plan, table_name) } end - def find_past_delayed_plans(time) + def find_execution_plan_dependencies(execution_plan_id) + table(:execution_plan_dependency) + .where(execution_plan_uuid: execution_plan_id) + .select_map(:blocked_by_uuid) + end + + def find_blocked_execution_plans(execution_plan_id) + table(:execution_plan_dependency) + .where(blocked_by_uuid: execution_plan_id) + .select_map(:execution_plan_uuid) + end + + def find_ready_delayed_plans(time) table_name = :delayed + # Subquery to find delayed plans that have at least one non-stopped dependency + plans_with_unfinished_deps = table(:execution_plan_dependency) + .join(TABLES[:execution_plan], uuid: :blocked_by_uuid) + .where(::Sequel.~(state: 'stopped')) + .select(:execution_plan_uuid) + records = with_retry do table(table_name) - .where(::Sequel.lit('start_at <= ? OR (start_before IS NOT NULL AND start_before <= ?)', time, time)) + .where(::Sequel.lit('start_at IS NULL OR (start_at <= ? OR (start_before IS NOT NULL AND start_before <= ?))', time, time)) .where(:frozen => false) + .exclude(execution_plan_uuid: plans_with_unfinished_deps) .order_by(:start_at) .all end @@ -175,6 +195,10 @@ def save_delayed_plan(execution_plan_id, value) save :delayed, { execution_plan_uuid: execution_plan_id }, value, with_data: false end + def chain_execution_plan(first, second) + save :execution_plan_dependency, {}, { execution_plan_uuid: second, blocked_by_uuid: first }, with_data: false + end + def load_step(execution_plan_id, step_id) load :step, execution_plan_uuid: execution_plan_id, id: step_id end @@ -319,7 +343,8 @@ def abort_if_pending_migrations! envelope: :dynflow_envelopes, coordinator_record: :dynflow_coordinator_records, delayed: :dynflow_delayed_plans, - output_chunk: :dynflow_output_chunks } + output_chunk: :dynflow_output_chunks, + execution_plan_dependency: :dynflow_execution_plan_dependencies } def table(which) db[TABLES.fetch(which)] diff --git a/lib/dynflow/persistence_adapters/sequel_migrations/025_create_execution_plan_dependencies.rb b/lib/dynflow/persistence_adapters/sequel_migrations/025_create_execution_plan_dependencies.rb new file mode 100644 index 000000000..69717d022 --- /dev/null +++ b/lib/dynflow/persistence_adapters/sequel_migrations/025_create_execution_plan_dependencies.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +Sequel.migration do + up do + type = database_type + create_table(:dynflow_execution_plan_dependencies) do + column_properties = if type.to_s.include?('postgres') + { type: :uuid } + else + { type: String, size: 36, fixed: true, null: false } + end + foreign_key :execution_plan_uuid, :dynflow_execution_plans, on_delete: :cascade, **column_properties + foreign_key :blocked_by_uuid, :dynflow_execution_plans, on_delete: :cascade, **column_properties + index :blocked_by_uuid + index :execution_plan_uuid + end + end + + down do + drop_table(:dynflow_execution_plan_dependencies) + end +end diff --git a/lib/dynflow/world.rb b/lib/dynflow/world.rb index 8d7433db1..2dc1856f6 100644 --- a/lib/dynflow/world.rb +++ b/lib/dynflow/world.rb @@ -202,6 +202,16 @@ def delay_with_options(action_class:, args:, delay_options:, id: nil, caller_act Scheduled[execution_plan.id] end + def chain(plan_uuids, action_class, *args) + plan_uuids = [plan_uuids] unless plan_uuids.is_a? Array + result = delay_with_options(action_class: action_class, args: args, delay_options: { frozen: true }) + plan_uuids.each do |plan_uuid| + persistence.chain_execution_plan(plan_uuid, result.execution_plan_id) + end + persistence.set_delayed_plan_frozen(result.execution_plan_id, false) + result + end + def plan_elsewhere(action_class, *args) execution_plan = ExecutionPlan.new(self, nil) execution_plan.delay(nil, action_class, {}, *args) diff --git a/test/future_execution_test.rb b/test/future_execution_test.rb index fee3377d5..ebe56c1c1 100644 --- a/test/future_execution_test.rb +++ b/test/future_execution_test.rb @@ -76,7 +76,7 @@ module FutureExecutionTest it 'finds delayed plans' do @start_at = Time.now.utc - 100 delayed_plan - past_delayed_plans = world.persistence.find_past_delayed_plans(@start_at + 10) + past_delayed_plans = world.persistence.find_ready_delayed_plans(@start_at + 10) _(past_delayed_plans.length).must_equal 1 _(past_delayed_plans.first.execution_plan_uuid).must_equal execution_plan.id end @@ -113,8 +113,8 @@ module FutureExecutionTest it 'checks for delayed plans in regular intervals' do start_time = klok.current_time - persistence.expect(:find_past_delayed_plans, [], [start_time]) - persistence.expect(:find_past_delayed_plans, [], [start_time + options[:poll_interval]]) + persistence.expect(:find_ready_delayed_plans, [], [start_time]) + persistence.expect(:find_ready_delayed_plans, [], [start_time + options[:poll_interval]]) dummy_world.stub :persistence, persistence do _(klok.pending_pings.length).must_equal 0 delayed_executor.start.wait @@ -190,6 +190,152 @@ module FutureExecutionTest _(serializer.args).must_equal args end end + + describe 'execution plan chaining' do + let(:world) do + WorldFactory.create_world { |config| config.auto_rescue = true } + end + + before do + @preexisting = world.persistence.find_ready_delayed_plans(Time.now).map(&:execution_plan_uuid) + end + + it 'chains two execution plans' do + plan1 = world.plan(Support::DummyExample::Dummy) + plan2 = world.chain(plan1.id, Support::DummyExample::Dummy) + + Concurrent::Promises.resolvable_future.tap do |promise| + world.execute(plan1.id, promise) + end.wait + + plan1 = world.persistence.load_execution_plan(plan1.id) + _(plan1.state).must_equal :stopped + ready = world.persistence.find_ready_delayed_plans(Time.now).reject { |p| @preexisting.include? p.execution_plan_uuid } + _(ready.count).must_equal 1 + _(ready.first.execution_plan_uuid).must_equal plan2.execution_plan_id + end + + it 'chains onto multiple execution plans and waits for all to finish' do + plan1 = world.plan(Support::DummyExample::Dummy) + plan2 = world.plan(Support::DummyExample::Dummy) + plan3 = world.chain([plan2.id, plan1.id], Support::DummyExample::Dummy) + + # Execute and complete plan1 + Concurrent::Promises.resolvable_future.tap do |promise| + world.execute(plan1.id, promise) + end.wait + + plan1 = world.persistence.load_execution_plan(plan1.id) + _(plan1.state).must_equal :stopped + + # plan3 should still not be ready because plan2 hasn't finished yet + ready = world.persistence.find_ready_delayed_plans(Time.now).reject { |p| @preexisting.include? p.execution_plan_uuid } + _(ready.count).must_equal 0 + + # Execute and complete plan2 + Concurrent::Promises.resolvable_future.tap do |promise| + world.execute(plan2.id, promise) + end.wait + + plan2 = world.persistence.load_execution_plan(plan2.id) + _(plan2.state).must_equal :stopped + + # Now plan3 should be ready since both plan1 and plan2 are complete + ready = world.persistence.find_ready_delayed_plans(Time.now).reject { |p| @preexisting.include? p.execution_plan_uuid } + _(ready.count).must_equal 1 + _(ready.first.execution_plan_uuid).must_equal plan3.execution_plan_id + end + + it 'cancels the chained plan if the prerequisite fails' do + plan1 = world.plan(Support::DummyExample::FailingDummy) + plan2 = world.chain(plan1.id, Support::DummyExample::Dummy) + + Concurrent::Promises.resolvable_future.tap do |promise| + world.execute(plan1.id, promise) + end.wait + + plan1 = world.persistence.load_execution_plan(plan1.id) + _(plan1.state).must_equal :stopped + _(plan1.result).must_equal :error + + # plan2 will appear in ready delayed plans + ready = world.persistence.find_ready_delayed_plans(Time.now).reject { |p| @preexisting.include? p.execution_plan_uuid } + _(ready.map(&:execution_plan_uuid)).must_equal [plan2.execution_plan_id] + + # Process the delayed plan through the director + work_item = Dynflow::Director::PlanningWorkItem.new(plan2.execution_plan_id, :default, world.id) + work_item.world = world + work_item.execute + + # Now plan2 should be stopped with error due to failed dependency + plan2 = world.persistence.load_execution_plan(plan2.execution_plan_id) + _(plan2.state).must_equal :stopped + _(plan2.result).must_equal :error + _(plan2.errors.first.message).must_match(/prerequisite execution plans failed/) + _(plan2.errors.first.message).must_match(/#{plan1.id}/) + end + + it 'cancels the chained plan if at least one prerequisite fails' do + plan1 = world.plan(Support::DummyExample::Dummy) + plan2 = world.plan(Support::DummyExample::FailingDummy) + plan3 = world.chain([plan1.id, plan2.id], Support::DummyExample::Dummy) + + # Execute and complete plan1 successfully + Concurrent::Promises.resolvable_future.tap do |promise| + world.execute(plan1.id, promise) + end.wait + + plan1 = world.persistence.load_execution_plan(plan1.id) + _(plan1.state).must_equal :stopped + _(plan1.result).must_equal :success + + # plan3 should still not be ready because plan2 hasn't finished yet + ready = world.persistence.find_ready_delayed_plans(Time.now).reject { |p| @preexisting.include? p.execution_plan_uuid } + _(ready).must_equal [] + + # Execute and complete plan2 with failure + Concurrent::Promises.resolvable_future.tap do |promise| + world.execute(plan2.id, promise) + end.wait + + plan2 = world.persistence.load_execution_plan(plan2.id) + _(plan2.state).must_equal :stopped + _(plan2.result).must_equal :error + + # plan3 will now appear in ready delayed plans even though one prerequisite failed + ready = world.persistence.find_ready_delayed_plans(Time.now).reject { |p| @preexisting.include? p.execution_plan_uuid } + _(ready.map(&:execution_plan_uuid)).must_equal [plan3.execution_plan_id] + + # Process the delayed plan through the director + work_item = Dynflow::Director::PlanningWorkItem.new(plan3.execution_plan_id, :default, world.id) + work_item.world = world + work_item.execute + + # Now plan3 should be stopped with error due to failed dependency + plan3 = world.persistence.load_execution_plan(plan3.execution_plan_id) + _(plan3.state).must_equal :stopped + _(plan3.result).must_equal :error + _(plan3.errors.first.message).must_match(/prerequisite execution plans failed/) + _(plan3.errors.first.message).must_match(/#{plan2.id}/) + end + + it 'chains runs the chained plan if the prerequisite was halted' do + plan1 = world.plan(Support::DummyExample::Dummy) + plan2 = world.chain(plan1.id, Support::DummyExample::Dummy) + + world.halt(plan1.id) + Concurrent::Promises.resolvable_future.tap do |promise| + world.execute(plan1.id, promise) + end.wait + + plan1 = world.persistence.load_execution_plan(plan1.id) + _(plan1.state).must_equal :stopped + _(plan1.result).must_equal :pending + ready = world.persistence.find_ready_delayed_plans(Time.now).reject { |p| @preexisting.include? p.execution_plan_uuid } + _(ready.count).must_equal 1 + _(ready.first.execution_plan_uuid).must_equal plan2.execution_plan_id + end + end end end end diff --git a/test/persistence_test.rb b/test/persistence_test.rb index aa0e87ecc..39841780f 100644 --- a/test/persistence_test.rb +++ b/test/persistence_test.rb @@ -342,7 +342,7 @@ def self.it_acts_as_persistence_adapter end end - describe '#find_past_delayed_plans' do + describe '#find_ready_delayed_plans' do it 'finds plans with start_before in past' do start_time = Time.now.utc prepare_and_save_plans @@ -352,7 +352,7 @@ def self.it_acts_as_persistence_adapter adapter.save_delayed_plan('plan3', :execution_plan_uuid => 'plan3', :frozen => false, :start_at => format_time(start_time + 60)) adapter.save_delayed_plan('plan4', :execution_plan_uuid => 'plan4', :frozen => false, :start_at => format_time(start_time - 60), :start_before => format_time(start_time - 60)) - plans = adapter.find_past_delayed_plans(start_time) + plans = adapter.find_ready_delayed_plans(start_time) _(plans.length).must_equal 3 _(plans.map { |plan| plan[:execution_plan_uuid] }).must_equal %w(plan2 plan4 plan1) end @@ -366,10 +366,77 @@ def self.it_acts_as_persistence_adapter adapter.save_delayed_plan('plan2', :execution_plan_uuid => 'plan2', :frozen => true, :start_at => format_time(start_time + 60), :start_before => format_time(start_time - 60)) - plans = adapter.find_past_delayed_plans(start_time) + plans = adapter.find_ready_delayed_plans(start_time) _(plans.length).must_equal 1 _(plans.first[:execution_plan_uuid]).must_equal 'plan1' end + + it 'finds plans with null start_at' do + start_time = Time.now.utc + prepare_and_save_plans + + adapter.save_delayed_plan('plan1', :execution_plan_uuid => 'plan1', :frozen => false) + + plans = adapter.find_ready_delayed_plans(start_time) + _(plans.length).must_equal 1 + _(plans.first[:execution_plan_uuid]).must_equal 'plan1' + end + + it 'properly stored execution plan dependencies' do + prepare_and_save_plans + + adapter.save_delayed_plan('plan1', :execution_plan_uuid => 'plan1', :frozen => false) + adapter.chain_execution_plan('plan2', 'plan1') + adapter.chain_execution_plan('plan3', 'plan1') + dependencies = adapter.find_execution_plan_dependencies('plan1') + _(dependencies.to_set).must_equal ['plan2', 'plan3'].to_set + end + + it 'does not find blocked plans' do + start_time = Time.now.utc + prepare_and_save_plans + + adapter.save_delayed_plan('plan1', :execution_plan_uuid => 'plan1', :frozen => false) + adapter.chain_execution_plan('plan2', 'plan1') + adapter.chain_execution_plan('plan3', 'plan1') + + plans = adapter.find_ready_delayed_plans(start_time) + _(plans.length).must_equal 0 + end + + it 'finds plans which are no longer blocked' do + start_time = Time.now.utc + prepare_and_save_plans + + adapter.save_delayed_plan('plan1', :execution_plan_uuid => 'plan1', :frozen => false) + adapter.chain_execution_plan('plan2', 'plan1') + + plans = adapter.find_ready_delayed_plans(start_time) + _(plans.length).must_equal 1 + _(plans.first[:execution_plan_uuid]).must_equal 'plan1' + end + + it 'does not find plans which are no longer blocked but are frozen' do + start_time = Time.now.utc + prepare_and_save_plans + + adapter.save_delayed_plan('plan1', :execution_plan_uuid => 'plan1', :frozen => true) + adapter.chain_execution_plan('plan2', 'plan1') + + plans = adapter.find_ready_delayed_plans(start_time) + _(plans.length).must_equal 0 + end + + it 'does not find plans which are no longer blocked but their start_at is in the future' do + start_time = Time.now.utc + prepare_and_save_plans + + adapter.save_delayed_plan('plan1', :execution_plan_uuid => 'plan1', :frozen => false, :start_at => start_time + 60) + adapter.chain_execution_plan('plan2', 'plan1') # plan2 is already stopped + + plans = adapter.find_ready_delayed_plans(start_time) + _(plans.length).must_equal 0 + end end describe '#delete_output_chunks' do diff --git a/test/support/dummy_example.rb b/test/support/dummy_example.rb index 94c3a9a71..89578c455 100644 --- a/test/support/dummy_example.rb +++ b/test/support/dummy_example.rb @@ -31,6 +31,10 @@ def run; end class FailingDummy < Dynflow::Action def run; raise 'error'; end + + def rescue_strategy + Dynflow::Action::Rescue::Fail + end end class Slow < Dynflow::Action diff --git a/web/views/show.erb b/web/views/show.erb index 7e895f686..cc2f3665f 100644 --- a/web/views/show.erb +++ b/web/views/show.erb @@ -43,6 +43,30 @@ <%= h(@plan.ended_at) %>

+<% dependencies = @plan.world.persistence.find_execution_plan_dependencies(@plan.id) %> +<% if dependencies.any? %> +

+ Depends on execution plans: +

+

+<% end %> + +<% blocked_plans = @plan.world.persistence.find_blocked_execution_plans(@plan.id) %> +<% if blocked_plans.any? %> +

+ Blocks execution plans: +

+

+<% end %> +