24
24
25
25
import logging
26
26
import sys
27
+ from dataclasses import dataclass
27
28
from typing import Any , Optional
28
29
29
30
import decoder .decoder as job_defintion_decoder
51
52
_LOGGER .addHandler (logging .StreamHandler (sys .stdout ))
52
53
53
54
55
+ @dataclass
56
+ class StepPreparationResponse :
57
+ """Step preparation response object. Iterations is +ve (non-zero) if a step
58
+ can be launched - it's value indicates how many times. If a step can be launched
59
+ 'variables' will not be None. If a parallel set of steps can take place
60
+ (even just one) 'iteration_variable' will be set and 'iteration_values'
61
+ will be a list containing a value for eacdh step."""
62
+
63
+ iterations : int
64
+ variables : dict [str , Any ] | None = None
65
+ iteration_variable : str | None = None
66
+ iteration_values : list [str ] | None = None
67
+
68
+
54
69
class WorkflowEngine :
55
70
"""The workflow engine."""
56
71
@@ -126,10 +141,18 @@ def _handle_workflow_start_message(self, r_wfid: str) -> None:
126
141
# Now find the first step (index 0)...
127
142
first_step : dict [str , Any ] = wf_response ["steps" ][0 ]
128
143
144
+ sp_resp = self ._prepare_step_variables (
145
+ wf = wf_response , step_definition = first_step , rwf = rwf_response
146
+ )
147
+ assert sp_resp .variables is not None
129
148
# Launch it.
130
149
# If there's a launch problem the step (and running workflow) will have
131
150
# and error, stopping it. There will be no Pod event as the launch has failed.
132
- self ._launch (wf = wf_response , rwf = rwf_response , step_definition = first_step )
151
+ self ._launch (
152
+ rwf = rwf_response ,
153
+ step_definition = first_step ,
154
+ step_preparation_response = sp_resp ,
155
+ )
133
156
134
157
def _handle_workflow_stop_message (self , r_wfid : str ) -> None :
135
158
"""Logic to handle a STOP message."""
@@ -265,8 +288,31 @@ def _handle_pod_message(self, msg: PodMessage) -> None:
265
288
# There's another step!
266
289
# For this simple logic it is the next step.
267
290
next_step = wf_response ["steps" ][step_index + 1 ]
291
+
292
+ # A mojor piece of work to accomplish is to get ourselves into a position
293
+ # that allows us to check the step command can be executed.
294
+ # We do this by compiling a map of variables we belive the step needs.
295
+
296
+ # If the step about to be launched is based on a prior step
297
+ # that generates multiple outputs (files) then we have to
298
+ # exit unless all of the step instances have completed.
299
+ #
300
+ # Do we need a 'prepare variables' function?
301
+ # One that returns a map of variables or nothing
302
+ # (e.g. 'nothing' when a step launch cannot be attempted)
303
+ sp_resp = self ._prepare_step_variables (
304
+ wf = wf_response , step_definition = next_step , rwf = rwf_response
305
+ )
306
+ if sp_resp .iterations == 0 :
307
+ # Cannot prepare variables for this step,
308
+ # we have to leave.
309
+ return
310
+ assert sp_resp .variables is not None
311
+
268
312
self ._launch (
269
- wf = wf_response , rwf = rwf_response , step_definition = next_step
313
+ rwf = rwf_response ,
314
+ step_definition = next_step ,
315
+ step_preparation_response = sp_resp ,
270
316
)
271
317
272
318
# Something was started (or there was a launch error and the step
@@ -361,20 +407,18 @@ def _validate_step_command(
361
407
)
362
408
return all_variables if success else message
363
409
364
- def _launch (
410
+ def _prepare_step_variables (
365
411
self ,
366
412
* ,
367
413
wf : dict [str , Any ],
368
- rwf : dict [str , Any ],
369
414
step_definition : dict [str , Any ],
370
- ) -> None :
415
+ rwf : dict [str , Any ],
416
+ ) -> StepPreparationResponse :
417
+ """Attempts to prepare a map of step variables. If variables cannot be
418
+ presented to the step we return an object with 'iterations' set to zero."""
419
+
371
420
step_name : str = step_definition ["name" ]
372
421
rwf_id : str = rwf ["id" ]
373
- project_id = rwf ["project" ]["id" ]
374
-
375
- # A mojor piece of work to accomplish is to get ourselves into a position
376
- # that allows us to check the step command can be executed.
377
- # We do this by compiling a map of variables we belive the step needs.
378
422
379
423
# We start with all the workflow variables that were provided
380
424
# by the user when they "ran" the workflow. We're given a full set of
@@ -390,13 +434,10 @@ def _launch(
390
434
msg = f"Failed command validation error_msg={ error_msg } "
391
435
_LOGGER .warning (msg )
392
436
self ._set_step_error (step_name , rwf_id , None , 1 , msg )
393
- return
437
+ return StepPreparationResponse ( iterations = 0 )
394
438
395
439
variables : dict [str , Any ] = error_or_variables
396
440
397
- # A step replication number,
398
- # used only for steps expected to run in parallel (even if just once)
399
- step_replication_number : int = 0
400
441
# Do we replicate this step (run it more than once)?
401
442
# We do if a variable in this step's mapping block
402
443
# refers to an output of a prior step whose type is 'files'.
@@ -405,7 +446,7 @@ def _launch(
405
446
#
406
447
# In this engine we onlhy act on the _first_ match, i.e. there CANNOT
407
448
# be more than one prior step variable that is 'files'!
408
- replication_values : list [str ] = []
449
+ iter_values : list [str ] = []
409
450
iter_variable : str | None = None
410
451
plumbing : dict [str , list [Connector ]] = get_step_prior_step_plumbing (
411
452
step_definition = step_definition
@@ -435,34 +476,59 @@ def _launch(
435
476
output_variable = connector .in_ ,
436
477
)
437
478
)
438
- replication_values = result ["output" ].copy ()
479
+ iter_values = result ["output" ].copy ()
439
480
break
440
481
# Stop if we've got an iteration variable
441
482
if iter_variable :
442
483
break
443
484
444
- num_step_instances : int = max (1 , len (replication_values ))
445
- for iteration in range (num_step_instances ):
485
+ num_step_instances : int = max (1 , len (iter_values ))
486
+ return StepPreparationResponse (
487
+ variables = variables ,
488
+ iterations = num_step_instances ,
489
+ iteration_variable = iter_variable ,
490
+ iteration_values = iter_values ,
491
+ )
492
+
493
+ def _launch (
494
+ self ,
495
+ * ,
496
+ rwf : dict [str , Any ],
497
+ step_definition : dict [str , Any ],
498
+ step_preparation_response : StepPreparationResponse ,
499
+ ) -> None :
500
+ step_name : str = step_definition ["name" ]
501
+ rwf_id : str = rwf ["id" ]
502
+ project_id = rwf ["project" ]["id" ]
503
+
504
+ # A step replication number,
505
+ # used only for steps expected to run in parallel (even if just once)
506
+ step_replication_number : int = 0
507
+
508
+ variables = step_preparation_response .variables
509
+ assert variables is not None
510
+ for iteration in range (step_preparation_response .iterations ):
446
511
447
512
# If we are replicating this step then we must replace the step's variable
448
513
# with a value expected for this iteration.
449
- if iter_variable :
450
- iter_value : str = replication_values [iteration ]
514
+ if step_preparation_response .iteration_variable :
515
+ assert step_preparation_response .iteration_values
516
+ iter_value : str = step_preparation_response .iteration_values [iteration ]
451
517
_LOGGER .info (
452
518
"Replicating step: %s iteration=%s variable=%s value=%s" ,
453
519
step_name ,
454
520
iteration ,
455
- iter_variable ,
521
+ step_preparation_response . iteration_variable ,
456
522
iter_value ,
457
523
)
458
524
# Over-write the replicating variable
459
525
# and set the replication number to a unique +ve non-zero value...
460
- variables [iter_variable ] = iter_value
526
+ variables [step_preparation_response . iteration_variable ] = iter_value
461
527
step_replication_number = iteration + 1
462
528
463
529
_LOGGER .info (
464
530
"Launching step: %s RunningWorkflow=%s (name=%s)"
465
- " variables =%s project=%s" ,
531
+ " step_variables =%s project=%s" ,
466
532
step_name ,
467
533
rwf_id ,
468
534
rwf ["name" ],
0 commit comments