@@ -190,6 +190,135 @@ module FutureExecutionTest
190190 _ ( serializer . args ) . must_equal args
191191 end
192192 end
193+
194+ describe 'execution plan chaining' do
195+ let ( :world ) do
196+ WorldFactory . create_world { |config | config . auto_rescue = true }
197+ end
198+
199+ before do
200+ @preexisting = world . persistence . find_ready_delayed_plans ( Time . now ) . map ( &:execution_plan_uuid )
201+ end
202+
203+ it 'chains two execution plans' do
204+ plan1 = world . plan ( Support ::DummyExample ::Dummy )
205+ plan2 = world . chain ( plan1 . id , Support ::DummyExample ::Dummy )
206+
207+ Concurrent ::Promises . resolvable_future . tap do |promise |
208+ world . execute ( plan1 . id , promise )
209+ end . wait
210+
211+ plan1 = world . persistence . load_execution_plan ( plan1 . id )
212+ _ ( plan1 . state ) . must_equal :stopped
213+ ready = world . persistence . find_ready_delayed_plans ( Time . now ) . reject { |p | @preexisting . include? p . execution_plan_uuid }
214+ _ ( ready . count ) . must_equal 1
215+ _ ( ready . first . execution_plan_uuid ) . must_equal plan2 . execution_plan_id
216+ end
217+
218+ it 'chains onto multiple execution plans and waits for all to finish' do
219+ plan1 = world . plan ( Support ::DummyExample ::Dummy )
220+ plan2 = world . plan ( Support ::DummyExample ::Dummy )
221+ plan3 = world . chain ( [ plan1 . id , plan2 . id ] , Support ::DummyExample ::Dummy )
222+
223+ # Execute and complete plan1
224+ Concurrent ::Promises . resolvable_future . tap do |promise |
225+ world . execute ( plan1 . id , promise )
226+ end . wait
227+
228+ plan1 = world . persistence . load_execution_plan ( plan1 . id )
229+ _ ( plan1 . state ) . must_equal :stopped
230+
231+ # plan3 should still not be ready because plan2 hasn't finished yet
232+ ready = world . persistence . find_ready_delayed_plans ( Time . now ) . reject { |p | @preexisting . include? p . execution_plan_uuid }
233+ _ ( ready . count ) . must_equal 0
234+
235+ # Execute and complete plan2
236+ Concurrent ::Promises . resolvable_future . tap do |promise |
237+ world . execute ( plan2 . id , promise )
238+ end . wait
239+
240+ plan2 = world . persistence . load_execution_plan ( plan2 . id )
241+ _ ( plan2 . state ) . must_equal :stopped
242+
243+ # Now plan3 should be ready since both plan1 and plan2 are complete
244+ ready = world . persistence . find_ready_delayed_plans ( Time . now ) . reject { |p | @preexisting . include? p . execution_plan_uuid }
245+ _ ( ready . count ) . must_equal 1
246+ _ ( ready . first . execution_plan_uuid ) . must_equal plan3 . execution_plan_id
247+ end
248+
249+ it 'cancels the chained plan if the prerequisite fails' do
250+ plan1 = world . plan ( Support ::DummyExample ::FailingDummy )
251+ plan2 = world . chain ( plan1 . id , Support ::DummyExample ::Dummy )
252+
253+ Concurrent ::Promises . resolvable_future . tap do |promise |
254+ world . execute ( plan1 . id , promise )
255+ end . wait
256+
257+ plan1 = world . persistence . load_execution_plan ( plan1 . id )
258+ _ ( plan1 . state ) . must_equal :stopped
259+ _ ( plan1 . result ) . must_equal :error
260+
261+ # plan2 will appear in ready delayed plans
262+ ready = world . persistence . find_ready_delayed_plans ( Time . now ) . reject { |p | @preexisting . include? p . execution_plan_uuid }
263+ _ ( ready . map ( &:execution_plan_uuid ) ) . must_equal [ plan2 . execution_plan_id ]
264+
265+ # Process the delayed plan through the director
266+ work_item = Dynflow ::Director ::PlanningWorkItem . new ( plan2 . execution_plan_id , :default , world . id )
267+ work_item . world = world
268+ work_item . execute
269+
270+ # Now plan2 should be stopped with error due to failed dependency
271+ plan2 = world . persistence . load_execution_plan ( plan2 . execution_plan_id )
272+ _ ( plan2 . state ) . must_equal :stopped
273+ _ ( plan2 . result ) . must_equal :error
274+ _ ( plan2 . errors . first . message ) . must_match ( /preqrequisite execution plans failed/ )
275+ _ ( plan2 . errors . first . message ) . must_match ( /#{ plan1 . id } / )
276+ end
277+
278+ it 'cancels the chained plan if at least one prerequisite fails' do
279+ plan1 = world . plan ( Support ::DummyExample ::Dummy )
280+ plan2 = world . plan ( Support ::DummyExample ::FailingDummy )
281+ plan3 = world . chain ( [ plan1 . id , plan2 . id ] , Support ::DummyExample ::Dummy )
282+
283+ # Execute and complete plan1 successfully
284+ Concurrent ::Promises . resolvable_future . tap do |promise |
285+ world . execute ( plan1 . id , promise )
286+ end . wait
287+
288+ plan1 = world . persistence . load_execution_plan ( plan1 . id )
289+ _ ( plan1 . state ) . must_equal :stopped
290+ _ ( plan1 . result ) . must_equal :success
291+
292+ # plan3 should still not be ready because plan2 hasn't finished yet
293+ ready = world . persistence . find_ready_delayed_plans ( Time . now ) . reject { |p | @preexisting . include? p . execution_plan_uuid }
294+ _ ( ready ) . must_equal [ ]
295+
296+ # Execute and complete plan2 with failure
297+ Concurrent ::Promises . resolvable_future . tap do |promise |
298+ world . execute ( plan2 . id , promise )
299+ end . wait
300+
301+ plan2 = world . persistence . load_execution_plan ( plan2 . id )
302+ _ ( plan2 . state ) . must_equal :stopped
303+ _ ( plan2 . result ) . must_equal :error
304+
305+ # plan3 will now appear in ready delayed plans even though one prerequisite failed
306+ ready = world . persistence . find_ready_delayed_plans ( Time . now ) . reject { |p | @preexisting . include? p . execution_plan_uuid }
307+ _ ( ready . map ( &:execution_plan_uuid ) ) . must_equal [ plan3 . execution_plan_id ]
308+
309+ # Process the delayed plan through the director
310+ work_item = Dynflow ::Director ::PlanningWorkItem . new ( plan3 . execution_plan_id , :default , world . id )
311+ work_item . world = world
312+ work_item . execute
313+
314+ # Now plan3 should be stopped with error due to failed dependency
315+ plan3 = world . persistence . load_execution_plan ( plan3 . execution_plan_id )
316+ _ ( plan3 . state ) . must_equal :stopped
317+ _ ( plan3 . result ) . must_equal :error
318+ _ ( plan3 . errors . first . message ) . must_match ( /preqrequisite execution plans failed/ )
319+ _ ( plan3 . errors . first . message ) . must_match ( /#{ plan2 . id } / )
320+ end
321+ end
193322 end
194323 end
195324end
0 commit comments