def test_executes_multiple_concurrent(self): """Independent tasks will be executed concurrently within the same iteration of the graph loop. """ g = TaskDependencyGraph(MockWorkflowContext()) task1 = tasks.NOPLocalWorkflowTask(None) task2 = tasks.NOPLocalWorkflowTask(None) g.add_task(task1) g.add_task(task2) with limited_sleep_mock(limit=1): g.execute() self.assertTrue(task1.is_terminated) self.assertTrue(task2.is_terminated)
def test_executes_single_task(self): """A single NOP task is executed within a single iteration of the tasks graph loop""" g = TaskDependencyGraph(MockWorkflowContext()) task = tasks.NOPLocalWorkflowTask(None) g.add_task(task) with limited_sleep_mock(limit=1): g.execute() self.assertTrue(task.is_terminated)
def execute_operation(ctx, operation, operation_kwargs, allow_kwargs_override, run_by_dependency_order, type_names, node_ids, node_instance_ids, **kwargs): """ A generic workflow for executing arbitrary operations on nodes """ graph = ctx.graph_mode() send_event_starting_tasks = {} send_event_done_tasks = {} # filtering node instances filtered_node_instances = [] for node in ctx.nodes: if node_ids and node.id not in node_ids: continue if type_names and not next((type_name for type_name in type_names if type_name in node.type_hierarchy), None): continue for instance in node.instances: if node_instance_ids and instance.id not in node_instance_ids: continue filtered_node_instances.append(instance) # pre-preparing events tasks for instance in filtered_node_instances: start_event_message = 'Starting operation {0}'.format(operation) if operation_kwargs: start_event_message += ' (Operation parameters: {0})'.format( operation_kwargs) send_event_starting_tasks[instance.id] = \ instance.send_event(start_event_message) send_event_done_tasks[instance.id] = \ instance.send_event('Finished operation {0}'.format(operation)) if run_by_dependency_order: # if run by dependency order is set, then create NOP tasks for the # rest of the instances. This is done to support indirect # dependencies, i.e. when instance A is dependent on instance B # which is dependent on instance C, where A and C are to be executed # with the operation on (i.e. they're in filtered_node_instances) # yet B isn't. # We add the NOP tasks rather than creating dependencies between A # and C themselves since even though it may sometimes increase the # number of dependency relationships in the execution graph, it also # ensures their number is linear to the number of relationships in # the deployment (e.g. consider if A and C are one out of N instances # of their respective nodes yet there's a single instance of B - # using NOP tasks we'll have 2N relationships instead of N^2). filtered_node_instances_ids = set(inst.id for inst in filtered_node_instances) for node in ctx.nodes: for instance in node.instances: if instance.id not in filtered_node_instances_ids: nop_task = workflow_tasks.NOPLocalWorkflowTask(ctx) send_event_starting_tasks[instance.id] = nop_task send_event_done_tasks[instance.id] = nop_task graph.add_task(nop_task) # preparing the parameters to the execute_operation call exec_op_params = dict() exec_op_params['kwargs'] = operation_kwargs exec_op_params['operation'] = operation if allow_kwargs_override is not None: exec_op_params['allow_kwargs_override'] = allow_kwargs_override # registering actual tasks to sequences for instance in filtered_node_instances: sequence = graph.sequence() sequence.add( send_event_starting_tasks[instance.id], instance.execute_operation(**exec_op_params), send_event_done_tasks[instance.id]) # adding tasks dependencies if required if run_by_dependency_order: for node in ctx.nodes: for instance in node.instances: for rel in instance.relationships: instance_starting_task = \ send_event_starting_tasks[instance.id] target_done_task = \ send_event_done_tasks.get(rel.target_id) graph.add_dependency(instance_starting_task, target_done_task) graph.execute()