def _has_active_tasks(self, liveaction_db, mistral_wf_state, mistral_tasks): # Identify if there are any active tasks in Mistral. active_mistral_tasks = len( [t for t in mistral_tasks if t['state'] in ACTIVE_STATES]) > 0 active_st2_tasks = False execution = ActionExecution.get(liveaction__id=str(liveaction_db.id)) for child_exec_id in execution.children: child_exec = ActionExecution.get(id=child_exec_id) # Catch exception where a child is requested twice due to st2mistral retrying # from a st2 API connection failure. The first child will be stuck in requested # while the mistral workflow is already completed. if (mistral_wf_state in DONE_STATES and child_exec.status == action_constants.LIVEACTION_STATUS_REQUESTED): continue if (child_exec.status not in action_constants.LIVEACTION_COMPLETED_STATES and child_exec.status != action_constants.LIVEACTION_STATUS_PAUSED): active_st2_tasks = True break if active_mistral_tasks: LOG.info('There are active mistral tasks for %s.', str(liveaction_db.id)) if active_st2_tasks: LOG.info('There are active st2 tasks for %s.', str(liveaction_db.id)) return active_mistral_tasks or active_st2_tasks
def _get_runner(self, runnertype_db, action_db, liveaction_db): runner = get_runner(runnertype_db.runner_module) resolved_entry_point = self._get_entry_point_abs_path(action_db.pack, action_db.entry_point) runner.runner_type_db = runnertype_db runner.container_service = RunnerContainerService() runner.action = action_db runner.action_name = action_db.name runner.liveaction = liveaction_db runner.liveaction_id = str(liveaction_db.id) runner.execution = ActionExecution.get(liveaction__id=runner.liveaction_id) runner.execution_id = str(runner.execution.id) runner.entry_point = resolved_entry_point runner.context = getattr(liveaction_db, 'context', dict()) runner.callback = getattr(liveaction_db, 'callback', dict()) runner.libs_dir_path = self._get_action_libs_abs_path(action_db.pack, action_db.entry_point) # For re-run, get the ActionExecutionDB in which the re-run is based on. rerun_ref_id = runner.context.get('re-run', {}).get('ref') runner.rerun_ex_ref = ActionExecution.get(id=rerun_ref_id) if rerun_ref_id else None return runner
def _get_runner(self, runnertype_db, action_db, liveaction_db): runner = get_runner(runnertype_db.runner_module) resolved_entry_point = self._get_entry_point_abs_path(action_db.pack, action_db.entry_point) runner.runner_type_db = runnertype_db runner.container_service = RunnerContainerService() runner.action = action_db runner.action_name = action_db.name runner.liveaction = liveaction_db runner.liveaction_id = str(liveaction_db.id) runner.execution = ActionExecution.get(liveaction__id=runner.liveaction_id) runner.execution_id = str(runner.execution.id) runner.entry_point = resolved_entry_point runner.context = getattr(liveaction_db, 'context', dict()) runner.callback = getattr(liveaction_db, 'callback', dict()) runner.libs_dir_path = self._get_action_libs_abs_path(action_db.pack, action_db.entry_point) # For re-run, get the ActionExecutionDB in which the re-run is based on. rerun_ref_id = runner.context.get('re-run', {}).get('ref') runner.rerun_ex_ref = ActionExecution.get(id=rerun_ref_id) if rerun_ref_id else None return runner
def _has_active_tasks(self, liveaction_db, mistral_wf_state, mistral_tasks): # Identify if there are any active tasks in Mistral. active_mistral_tasks = len([t for t in mistral_tasks if t['state'] in ACTIVE_STATES]) > 0 active_st2_tasks = False execution = ActionExecution.get(liveaction__id=str(liveaction_db.id)) for child_exec_id in execution.children: child_exec = ActionExecution.get(id=child_exec_id) # Catch exception where a child is requested twice due to st2mistral retrying # from a st2 API connection failure. The first child will be stuck in requested # while the mistral workflow is already completed. if (mistral_wf_state in DONE_STATES and child_exec.status == action_constants.LIVEACTION_STATUS_REQUESTED): continue if (child_exec.status not in action_constants.LIVEACTION_COMPLETED_STATES and child_exec.status != action_constants.LIVEACTION_STATUS_PAUSED): active_st2_tasks = True break if active_mistral_tasks: LOG.info('There are active mistral tasks for %s.', str(liveaction_db.id)) if active_st2_tasks: LOG.info('There are active st2 tasks for %s.', str(liveaction_db.id)) return active_mistral_tasks or active_st2_tasks
def _has_active_tasks(self, liveaction_db, mistral_tasks): # Identify if there are any active tasks in Mistral. active_mistral_tasks = len( [t for t in mistral_tasks if t['state'] in ACTIVE_STATES]) > 0 active_st2_tasks = False execution = ActionExecution.get(liveaction__id=str(liveaction_db.id)) for child_exec_id in execution.children: child_exec = ActionExecution.get(id=child_exec_id) if (child_exec.status not in action_constants.LIVEACTION_COMPLETED_STATES and child_exec.status != action_constants.LIVEACTION_STATUS_PAUSED): active_st2_tasks = True break if active_mistral_tasks: LOG.info('There are active mistral tasks for %s.', str(liveaction_db.id)) if active_st2_tasks: LOG.info('There are active st2 tasks for %s.', str(liveaction_db.id)) return active_mistral_tasks or active_st2_tasks
def run(self, action_parameters): liveaction_db = action_utils.get_liveaction_by_id(self.liveaction_id) exc = ActionExecution.get(liveaction__id=str(liveaction_db.id)) # Assemble and dispatch trigger trigger_ref = ResourceReference.to_string_reference( pack=INQUIRY_TRIGGER['pack'], name=INQUIRY_TRIGGER['name'] ) trigger_payload = { "id": str(exc.id), "route": self.route } self.trigger_dispatcher.dispatch(trigger_ref, trigger_payload) # We only want to request a pause if this has a parent if liveaction_db.context.get("parent"): # Get the root liveaction and request that it pauses root_liveaction = action_service.get_root_liveaction(liveaction_db) action_service.request_pause( root_liveaction, self.context.get('user', None) ) result = { "schema": self.schema, "roles": self.roles_param, "users": self.users_param, "route": self.route, "ttl": self.ttl } return (LIVEACTION_STATUS_PENDING, result, None)
def _resume_action(self, liveaction_db): action_execution_db = ActionExecution.get(liveaction__id=str(liveaction_db.id)) extra = {'action_execution_db': action_execution_db, 'liveaction_db': liveaction_db} LOG.audit('Resuming action execution.', extra=extra) # the extra field will not be shown in non-audit logs so temporarily log at info. LOG.info('Dispatched {~}action_execution: %s / {~}live_action: %s with "%s" status.', action_execution_db.id, liveaction_db.id, liveaction_db.status) try: result = self.container.dispatch(liveaction_db) LOG.debug('Runner dispatch produced result: %s', result) except: _, ex, tb = sys.exc_info() extra['error'] = str(ex) LOG.info('Failed to resume action execution %s.' % (liveaction_db.id), extra=extra) raise # Cascade the resume upstream if action execution is child of an orquesta workflow. # The action service request_resume function is not used here because we do not want # other peer subworkflows to be resumed. if 'orquesta' in action_execution_db.context and 'parent' in action_execution_db.context: wf_svc.handle_action_execution_resume(action_execution_db) return result
def _format_action_exec_result(self, action_node, liveaction_db, created_at, updated_at, error=None): """ Format ActionExecution result so it can be used in the final action result output. :rtype: ``dict`` """ assert(isinstance(created_at, datetime.datetime)) assert(isinstance(updated_at, datetime.datetime)) result = {} execution_db = None if liveaction_db: execution_db = ActionExecution.get(liveaction__id=str(liveaction_db.id)) result['id'] = action_node.name result['name'] = action_node.name result['execution_id'] = str(execution_db.id) if execution_db else None result['workflow'] = None result['created_at'] = isotime.format(dt=created_at) result['updated_at'] = isotime.format(dt=updated_at) if error or not liveaction_db: result['state'] = LIVEACTION_STATUS_FAILED else: result['state'] = liveaction_db.status if error: result['result'] = error else: result['result'] = liveaction_db.result return result
def request_cancellation(liveaction, requester): """ Request cancellation of an action execution. :return: (liveaction, execution) :rtype: tuple """ if liveaction.status == action_constants.LIVEACTION_STATUS_CANCELING: return liveaction if liveaction.status not in action_constants.LIVEACTION_CANCELABLE_STATES: raise Exception( 'Unable to cancel liveaction "%s" because it is already in a ' 'completed state.' % liveaction.id ) result = { 'message': 'Action canceled by user.', 'user': requester } # Run cancelation sequence for liveaction that is in running state or # if the liveaction is operating under a workflow. if ('parent' in liveaction.context or liveaction.status in action_constants.LIVEACTION_STATUS_RUNNING): status = action_constants.LIVEACTION_STATUS_CANCELING else: status = action_constants.LIVEACTION_STATUS_CANCELED liveaction = update_status(liveaction, status, result=result) execution = ActionExecution.get(liveaction__id=str(liveaction.id)) return (liveaction, execution)
def request_cancellation(liveaction, requester): """ Request cancellation of an action execution. :return: (liveaction, execution) :rtype: tuple """ if liveaction.status == action_constants.LIVEACTION_STATUS_CANCELING: return liveaction if liveaction.status not in action_constants.LIVEACTION_CANCELABLE_STATES: raise Exception( 'Unable to cancel liveaction "%s" because it is already in a ' 'completed state.' % liveaction.id) result = {'message': 'Action canceled by user.', 'user': requester} # Run cancelation sequence for liveaction that is in running state or # if the liveaction is operating under a workflow. if ('parent' in liveaction.context or liveaction.status in action_constants.LIVEACTION_STATUS_RUNNING): status = action_constants.LIVEACTION_STATUS_CANCELING else: status = action_constants.LIVEACTION_STATUS_CANCELED liveaction = update_status(liveaction, status, result=result) execution = ActionExecution.get(liveaction__id=str(liveaction.id)) return (liveaction, execution)
def request_cancellation(liveaction, requester): """ Request cancellation of an action execution. :return: (liveaction, execution) :rtype: tuple """ if liveaction.status == action_constants.LIVEACTION_STATUS_CANCELING: return liveaction if liveaction.status not in action_constants.LIVEACTION_CANCELABLE_STATES: raise Exception( 'Unable to cancel liveaction "%s" because it is already in a ' 'completed state.' % liveaction.id) result = {'message': 'Action canceled by user.', 'user': requester} # There is real work only when liveaction is still running. status = (action_constants.LIVEACTION_STATUS_CANCELING if liveaction.status == action_constants.LIVEACTION_STATUS_RUNNING else action_constants.LIVEACTION_STATUS_CANCELED) liveaction = update_status(liveaction, status, result=result) execution = ActionExecution.get(liveaction__id=str(liveaction.id)) return (liveaction, execution)
def cancel(self): mistral_ctx = self.context.get('mistral', dict()) if not mistral_ctx.get('execution_id'): raise Exception( 'Unable to cancel because mistral execution_id is missing.') # Cancels the main workflow execution. Any non-workflow tasks that are still # running will be allowed to complete gracefully. self._client.executions.update(mistral_ctx.get('execution_id'), 'CANCELLED') # If workflow is executed under another parent workflow, cancel the corresponding # action execution for the task in the parent workflow. if 'parent' in getattr(self, 'context', {}) and mistral_ctx.get('action_execution_id'): mistral_action_ex_id = mistral_ctx.get('action_execution_id') self._client.action_executions.update(mistral_action_ex_id, 'CANCELLED') # Identify the list of action executions that are workflows and still running. for child_exec_id in self.execution.children: child_exec = ActionExecution.get(id=child_exec_id) if (child_exec.runner['name'] in action_constants.WORKFLOW_RUNNER_TYPES and child_exec.status in action_constants.LIVEACTION_CANCELABLE_STATES): action_service.request_cancellation( LiveAction.get(id=child_exec.liveaction['id']), self.context.get('user', None)) return (action_constants.LIVEACTION_STATUS_CANCELING, self.liveaction.result, self.liveaction.context)
def update_execution(liveaction_db, publish=True, set_result_size=False): """ :param set_result_size: True to calculate size of the serialized result field value and set it on the "result_size" database field. """ execution = ActionExecution.get(liveaction__id=str(liveaction_db.id)) decomposed = _decompose_liveaction(liveaction_db) kw = {} for k, v in six.iteritems(decomposed): kw["set__" + k] = v if liveaction_db.status != execution.status: # Note: If the status changes we store this transition in the "log" attribute of action # execution kw["push__log"] = _create_execution_log_entry(liveaction_db.status) if set_result_size: # Sadly with the current ORM abstraction there is no better way to achieve updating # result_size and we need to serialize the value again - luckily that operation is fast. # To put things into perspective - on 4 MB result dictionary it only takes 7 ms which is # negligible compared to other DB operations duration (and for smaller results it takes # in sub ms range). with Timer(key="action.executions.calculate_result_size"): result_size = len( ActionExecutionDB.result._serialize_field_value(liveaction_db.result) ) kw["set__result_size"] = result_size execution = ActionExecution.update(execution, publish=publish, **kw) return execution
def _get_execution_for_liveaction(self, liveaction): execution = ActionExecution.get(liveaction__id=str(liveaction.id)) if not execution: return None return execution
def run(self, action_parameters): liveaction_db = action_utils.get_liveaction_by_id(self.liveaction_id) exc = ActionExecution.get(liveaction__id=str(liveaction_db.id)) # Assemble and dispatch trigger trigger_ref = ResourceReference.to_string_reference( pack=INQUIRY_TRIGGER['pack'], name=INQUIRY_TRIGGER['name']) trigger_payload = {"id": str(exc.id), "route": self.route} self.trigger_dispatcher.dispatch(trigger_ref, trigger_payload) # We only want to request a pause if this has a parent if liveaction_db.context.get("parent"): # Get the root liveaction and request that it pauses root_liveaction = action_service.get_root_liveaction(liveaction_db) action_service.request_pause(root_liveaction, self.context.get('user', None)) result = { "schema": self.schema, "roles": self.roles_param, "users": self.users_param, "route": self.route, "ttl": self.ttl } return (LIVEACTION_STATUS_PENDING, result, None)
def test_chained_executions(self): liveaction = LiveActionDB(action='core.chain') liveaction, _ = action_service.request(liveaction) liveaction = LiveAction.get_by_id(str(liveaction.id)) self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_FAILED) execution = self._get_action_execution(liveaction__id=str(liveaction.id), raise_exception=True) action = action_utils.get_action_by_ref('core.chain') self.assertDictEqual(execution.action, vars(ActionAPI.from_model(action))) runner = RunnerType.get_by_name(action.runner_type['name']) self.assertDictEqual(execution.runner, vars(RunnerTypeAPI.from_model(runner))) liveaction = LiveAction.get_by_id(str(liveaction.id)) self.assertEqual(execution.start_timestamp, liveaction.start_timestamp) self.assertEqual(execution.end_timestamp, liveaction.end_timestamp) self.assertEqual(execution.result, liveaction.result) self.assertEqual(execution.status, liveaction.status) self.assertEqual(execution.context, liveaction.context) self.assertEqual(execution.liveaction['callback'], liveaction.callback) self.assertEqual(execution.liveaction['action'], liveaction.action) self.assertGreater(len(execution.children), 0) for child in execution.children: record = ActionExecution.get(id=child, raise_exception=True) self.assertEqual(record.parent, str(execution.id)) self.assertEqual(record.action['name'], 'local') self.assertEqual(record.runner['name'], 'run-local')
def update_execution(liveaction_db, publish=True): execution = ActionExecution.get(liveaction__id=str(liveaction_db.id)) decomposed = _decompose_liveaction(liveaction_db) for k, v in six.iteritems(decomposed): setattr(execution, k, v) execution = ActionExecution.add_or_update(execution, publish=publish) return execution
def test_chained_executions(self): with mock.patch("st2common.runners.register_runner", mock.MagicMock(return_value=action_chain_runner)): liveaction = LiveActionDB(action="executions.chain") liveaction, _ = action_service.request(liveaction) liveaction = LiveAction.get_by_id(str(liveaction.id)) self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_FAILED) execution = self._get_action_execution(liveaction__id=str(liveaction.id), raise_exception=True) action = action_utils.get_action_by_ref("executions.chain") self.assertDictEqual(execution.action, vars(ActionAPI.from_model(action))) runner = RunnerType.get_by_name(action.runner_type["name"]) self.assertDictEqual(execution.runner, vars(RunnerTypeAPI.from_model(runner))) liveaction = LiveAction.get_by_id(str(liveaction.id)) self.assertEqual(execution.start_timestamp, liveaction.start_timestamp) self.assertEqual(execution.end_timestamp, liveaction.end_timestamp) self.assertEqual(execution.result, liveaction.result) self.assertEqual(execution.status, liveaction.status) self.assertEqual(execution.context, liveaction.context) self.assertEqual(execution.liveaction["callback"], liveaction.callback) self.assertEqual(execution.liveaction["action"], liveaction.action) self.assertGreater(len(execution.children), 0) for child in execution.children: record = ActionExecution.get(id=child, raise_exception=True) self.assertEqual(record.parent, str(execution.id)) self.assertEqual(record.action["name"], "local") self.assertEqual(record.runner["name"], "run-local")
def _resume_action(self, liveaction_db): action_execution_db = ActionExecution.get( liveaction__id=str(liveaction_db.id)) extra = { 'action_execution_db': action_execution_db, 'liveaction_db': liveaction_db } LOG.audit('Resuming action execution.', extra=extra) # the extra field will not be shown in non-audit logs so temporarily log at info. LOG.info( 'Dispatched {~}action_execution: %s / {~}live_action: %s with "%s" status.', action_execution_db.id, liveaction_db.id, liveaction_db.status) try: result = self.container.dispatch(liveaction_db) LOG.debug('Runner dispatch produced result: %s', result) except: _, ex, tb = sys.exc_info() extra['error'] = str(ex) LOG.info('Failed to resume action execution %s.' % (liveaction_db.id), extra=extra) raise return result
def request_cancellation(liveaction, requester): """ Request cancellation of an action execution. :return: (liveaction, execution) :rtype: tuple """ if liveaction.status == action_constants.LIVEACTION_STATUS_CANCELING: return liveaction if liveaction.status not in action_constants.LIVEACTION_CANCELABLE_STATES: raise Exception('Unable to cancel execution because it is already in a completed state.') result = { 'message': 'Action canceled by user.', 'user': requester } # There is real work only when liveaction is still running. status = (action_constants.LIVEACTION_STATUS_CANCELING if liveaction.status == action_constants.LIVEACTION_STATUS_RUNNING else action_constants.LIVEACTION_STATUS_CANCELED) update_status(liveaction, status, result=result) execution = ActionExecution.get(liveaction__id=str(liveaction.id)) return (liveaction, execution)
def _get_execution_for_liveaction(self, liveaction): execution = ActionExecution.get(liveaction__id=str(liveaction.id)) if not execution: return None return execution
def _pause_action(self, liveaction_db): action_execution_db = ActionExecution.get( liveaction__id=str(liveaction_db.id)) extra = { "action_execution_db": action_execution_db, "liveaction_db": liveaction_db, } LOG.audit("Pausing action execution.", extra=extra) # the extra field will not be shown in non-audit logs so temporarily log at info. LOG.info( 'Dispatched {~}action_execution: %s / {~}live_action: %s with "%s" status.', action_execution_db.id, liveaction_db.id, liveaction_db.status, ) try: result = self.container.dispatch(liveaction_db) LOG.debug("Runner dispatch produced result: %s", result) except: _, ex, tb = sys.exc_info() extra["error"] = str(ex) LOG.info("Failed to pause action execution %s." % (liveaction_db.id), extra=extra) raise return result
def _resume_action(self, liveaction_db): action_execution_db = ActionExecution.get(liveaction__id=str(liveaction_db.id)) extra = {'action_execution_db': action_execution_db, 'liveaction_db': liveaction_db} LOG.audit('Resuming action execution.', extra=extra) # the extra field will not be shown in non-audit logs so temporarily log at info. LOG.info('Dispatched {~}action_execution: %s / {~}live_action: %s with "%s" status.', action_execution_db.id, liveaction_db.id, liveaction_db.status) try: result = self.container.dispatch(liveaction_db) LOG.debug('Runner dispatch produced result: %s', result) except: _, ex, tb = sys.exc_info() extra['error'] = str(ex) LOG.info('Failed to resume action execution %s.' % (liveaction_db.id), extra=extra) raise # Cascade the resume upstream if action execution is child of an orquesta workflow. # The action service request_resume function is not used here because we do not want # other peer subworkflows to be resumed. if 'orquesta' in action_execution_db.context and 'parent' in action_execution_db.context: wf_svc.handle_action_execution_resume(action_execution_db) return result
def resume(self): mistral_ctx = self.context.get('mistral', dict()) if not mistral_ctx.get('execution_id'): raise Exception('Unable to resume because mistral execution_id is missing.') # If workflow is executed under another parent workflow, resume the corresponding # action execution for the task in the parent workflow. if 'parent' in getattr(self, 'context', {}) and mistral_ctx.get('action_execution_id'): mistral_action_ex_id = mistral_ctx.get('action_execution_id') self._client.action_executions.update(mistral_action_ex_id, 'RUNNING') # Pause the main workflow execution. Any non-workflow tasks that are still # running will be allowed to complete gracefully. self._client.executions.update(mistral_ctx.get('execution_id'), 'RUNNING') # Identify the list of action executions that are workflows and cascade resume. for child_exec_id in self.execution.children: child_exec = ActionExecution.get(id=child_exec_id, raise_exception=True) if (child_exec.runner['name'] in action_constants.WORKFLOW_RUNNER_TYPES and child_exec.status == action_constants.LIVEACTION_STATUS_PAUSED): action_service.request_resume( LiveAction.get(id=child_exec.liveaction['id']), self.context.get('user', None) ) return ( action_constants.LIVEACTION_STATUS_RUNNING, self.execution.result, self.execution.context )
def _format_action_exec_result(self, action_node, liveaction_db, created_at, updated_at, error=None): """ Format ActionExecution result so it can be used in the final action result output. :rtype: ``dict`` """ assert isinstance(created_at, datetime.datetime) assert isinstance(updated_at, datetime.datetime) result = {} execution_db = None if liveaction_db: execution_db = ActionExecution.get(liveaction__id=str(liveaction_db.id)) result['id'] = action_node.name result['name'] = action_node.name result['execution_id'] = str(execution_db.id) if execution_db else None result['workflow'] = None result['created_at'] = isotime.format(dt=created_at) result['updated_at'] = isotime.format(dt=updated_at) if error or not liveaction_db: result['state'] = LIVEACTION_STATUS_FAILED else: result['state'] = liveaction_db.status if error: result['result'] = error else: result['result'] = liveaction_db.result return result
def _get_execution_id(self, liveaction): try: execution = ActionExecution.get(liveaction__id=str(liveaction.id)) return str(execution.id) except: LOG.exception('Execution object corresponding to LiveAction %s not found.', str(liveaction.id)) return None
def update_status(liveaction_api, liveaction_db): status = liveaction_api.status result = getattr(liveaction_api, 'result', None) liveaction_db = action_service.update_status( liveaction_db, status, result) actionexecution_db = ActionExecution.get( liveaction__id=str(liveaction_db.id)) return (liveaction_db, actionexecution_db)
def request_resume(liveaction, requester): """ Request resume for a paused action execution. :return: (liveaction, execution) :rtype: tuple """ # Validate that the runner type of the action supports pause. action_db = action_utils.get_action_by_ref(liveaction.action) if not action_db: raise ValueError( 'Unable to resume liveaction "%s" because the action "%s" ' "is not found." % (liveaction.id, liveaction.action)) if action_db.runner_type[ "name"] not in action_constants.WORKFLOW_RUNNER_TYPES: raise runner_exc.InvalidActionRunnerOperationError( 'Unable to resume liveaction "%s" because it is not supported by the ' '"%s" runner.' % (liveaction.id, action_db.runner_type["name"])) running_states = [ action_constants.LIVEACTION_STATUS_RUNNING, action_constants.LIVEACTION_STATUS_RESUMING, ] if liveaction.status in running_states: execution = ActionExecution.get(liveaction__id=str(liveaction.id)) return (liveaction, execution) if liveaction.status != action_constants.LIVEACTION_STATUS_PAUSED: raise runner_exc.UnexpectedActionExecutionStatusError( 'Unable to resume liveaction "%s" because it is in "%s" state and ' 'not in "paused" state.' % (liveaction.id, liveaction.status)) liveaction.context["resumed_by"] = get_requester(requester) liveaction = update_status( liveaction, action_constants.LIVEACTION_STATUS_RESUMING, context=liveaction.context, ) execution = ActionExecution.get(liveaction__id=str(liveaction.id)) return (liveaction, execution)
def update_status(liveaction_api, liveaction_db): status = liveaction_api.status result = getattr(liveaction_api, "result", None) liveaction_db = action_service.update_status(liveaction_db, status, result, set_result_size=True) actionexecution_db = ActionExecution.get( liveaction__id=str(liveaction_db.id)) return (liveaction_db, actionexecution_db)
def request_resume(liveaction, requester): """ Request resume for a paused action execution. :return: (liveaction, execution) :rtype: tuple """ # Validate that the runner type of the action supports pause. action_db = action_utils.get_action_by_ref(liveaction.action) if not action_db: raise ValueError( 'Unable to resume liveaction "%s" because the action "%s" ' 'is not found.' % (liveaction.id, liveaction.action) ) if action_db.runner_type['name'] not in action_constants.WORKFLOW_RUNNER_TYPES: raise runner_exc.InvalidActionRunnerOperationError( 'Unable to resume liveaction "%s" because it is not supported by the ' '"%s" runner.' % (liveaction.id, action_db.runner_type['name']) ) running_states = [ action_constants.LIVEACTION_STATUS_RUNNING, action_constants.LIVEACTION_STATUS_RESUMING ] if liveaction.status in running_states: execution = ActionExecution.get(liveaction__id=str(liveaction.id)) return (liveaction, execution) if liveaction.status != action_constants.LIVEACTION_STATUS_PAUSED: raise runner_exc.UnexpectedActionExecutionStatusError( 'Unable to resume liveaction "%s" because it is in "%s" state and ' 'not in "paused" state.' % (liveaction.id, liveaction.status) ) liveaction = update_status(liveaction, action_constants.LIVEACTION_STATUS_RESUMING) execution = ActionExecution.get(liveaction__id=str(liveaction.id)) return (liveaction, execution)
def _get_runner(self, runnertype_db, action_db, liveaction_db): resolved_entry_point = self._get_entry_point_abs_path( action_db.pack, action_db.entry_point) context = getattr(liveaction_db, 'context', dict()) user = context.get('user', cfg.CONF.system_user.user) # Note: Right now configs are only supported by the Python runner actions if runnertype_db.runner_module == 'python_runner': LOG.debug('Loading config for pack') config_loader = ContentPackConfigLoader(pack_name=action_db.pack, user=user) config = config_loader.get_config() else: config = None runner = get_runner(package_name=runnertype_db.runner_package, module_name=runnertype_db.runner_module, config=config) # TODO: Pass those arguments to the constructor instead of late # assignment, late assignment is awful runner.runner_type_db = runnertype_db runner.action = action_db runner.action_name = action_db.name runner.liveaction = liveaction_db runner.liveaction_id = str(liveaction_db.id) runner.execution = ActionExecution.get( liveaction__id=runner.liveaction_id) runner.execution_id = str(runner.execution.id) runner.entry_point = resolved_entry_point runner.context = context runner.callback = getattr(liveaction_db, 'callback', dict()) runner.libs_dir_path = self._get_action_libs_abs_path( action_db.pack, action_db.entry_point) # For re-run, get the ActionExecutionDB in which the re-run is based on. rerun_ref_id = runner.context.get('re-run', {}).get('ref') runner.rerun_ex_ref = ActionExecution.get( id=rerun_ref_id) if rerun_ref_id else None return runner
def get_trace_db_by_live_action(liveaction): """ Given a liveaction does the best attempt to return a TraceDB. 1. From trace_context in liveaction.context 2. From parent in liveaction.context 3. From action_execution associated with provided liveaction 4. Creates a new TraceDB (which calling method is on the hook to persist). :param liveaction: liveaction from which to figure out a TraceDB. :type liveaction: ``LiveActionDB`` :returns: (boolean, TraceDB) if the TraceDB was created(but not saved to DB) or retrieved from the DB and the TraceDB itself. :rtype: ``tuple`` """ trace_db = None created = False # 1. Try to get trace_db from liveaction context. # via trigger_instance + rule or via user specified trace_context trace_context = liveaction.context.get(TRACE_CONTEXT, None) if trace_context: trace_context = _get_valid_trace_context(trace_context) trace_db = get_trace(trace_context=trace_context, ignore_trace_tag=True) # found a trace_context but no trace_db. This implies a user supplied # trace_tag so create a new trace_db if not trace_db: trace_db = TraceDB(trace_tag=trace_context.trace_tag) created = True return (created, trace_db) # 2. If not found then check if parent context contains an execution_id. # This cover case for child execution of a workflow. parent_context = executions.get_parent_context(liveaction_db=liveaction) if not trace_context and parent_context: parent_execution_id = parent_context.get("execution_id", None) if parent_execution_id: # go straight to a trace_db. If there is a parent execution then that must # be associated with a Trace. trace_db = get_trace_db_by_action_execution( action_execution_id=parent_execution_id) if not trace_db: raise StackStormDBObjectNotFoundError( "No trace found for execution %s" % parent_execution_id) return (created, trace_db) # 3. Check if the action_execution associated with liveaction leads to a trace_db execution = ActionExecution.get(liveaction__id=str(liveaction.id)) if execution: trace_db = get_trace_db_by_action_execution(action_execution=execution) # 4. No trace_db found, therefore create one. This typically happens # when execution is run by hand. if not trace_db: trace_db = TraceDB(trace_tag="execution-%s" % str(liveaction.id)) created = True return (created, trace_db)
def request_pause(liveaction, requester): """ Request pause for a running action execution. :return: (liveaction, execution) :rtype: tuple """ # Validate that the runner type of the action supports pause. action_db = action_utils.get_action_by_ref(liveaction.action) if not action_db: raise ValueError( 'Unable to pause liveaction "%s" because the action "%s" ' "is not found." % (liveaction.id, liveaction.action) ) if action_db.runner_type["name"] not in action_constants.WORKFLOW_RUNNER_TYPES: raise runner_exc.InvalidActionRunnerOperationError( 'Unable to pause liveaction "%s" because it is not supported by the ' '"%s" runner.' % (liveaction.id, action_db.runner_type["name"]) ) if ( liveaction.status == action_constants.LIVEACTION_STATUS_PAUSING or liveaction.status == action_constants.LIVEACTION_STATUS_PAUSED ): execution = ActionExecution.get(liveaction__id=str(liveaction.id)) return (liveaction, execution) if liveaction.status != action_constants.LIVEACTION_STATUS_RUNNING: raise runner_exc.UnexpectedActionExecutionStatusError( 'Unable to pause liveaction "%s" because it is not in a running state.' % liveaction.id ) liveaction = update_status(liveaction, action_constants.LIVEACTION_STATUS_PAUSING) execution = ActionExecution.get(liveaction__id=str(liveaction.id)) return (liveaction, execution)
def get_trace_db_by_live_action(liveaction): """ Given a liveaction does the best attempt to return a TraceDB. 1. From trace_context in liveaction.context 2. From parent in liveaction.context 3. From action_execution associated with provided liveaction 4. Creates a new TraceDB (which calling method is on the hook to persist). :param liveaction: liveaction from which to figure out a TraceDB. :type liveaction: ``LiveActionDB`` :returns: (boolean, TraceDB) if the TraceDB was created(but not saved to DB) or retrieved from the DB and the TraceDB itself. :rtype: ``tuple`` """ trace_db = None created = False # 1. Try to get trace_db from liveaction context. # via trigger_instance + rule or via user specified trace_context trace_context = liveaction.context.get(TRACE_CONTEXT, None) if trace_context: trace_context = _get_valid_trace_context(trace_context) trace_db = get_trace(trace_context=trace_context, ignore_trace_tag=True) # found a trace_context but no trace_db. This implies a user supplied # trace_tag so create a new trace_db if not trace_db: trace_db = TraceDB(trace_tag=trace_context.trace_tag) created = True return (created, trace_db) # 2. If not found then check if parent context contains an execution_id. # This cover case for child execution of a workflow. parent_context = executions.get_parent_context(liveaction_db=liveaction) if not trace_context and parent_context: parent_execution_id = parent_context.get('execution_id', None) if parent_execution_id: # go straight to a trace_db. If there is a parent execution then that must # be associated with a Trace. trace_db = get_trace_db_by_action_execution(action_execution_id=parent_execution_id) if not trace_db: raise StackStormDBObjectNotFoundError('No trace found for execution %s' % parent_execution_id) return (created, trace_db) # 3. Check if the action_execution associated with liveaction leads to a trace_db execution = ActionExecution.get(liveaction__id=str(liveaction.id)) if execution: trace_db = get_trace_db_by_action_execution(action_execution=execution) # 4. No trace_db found, therefore create one. This typically happens # when execution is run by hand. if not trace_db: trace_db = TraceDB(trace_tag='execution-%s' % str(liveaction.id)) created = True return (created, trace_db)
def _get_runner(self, runner_type_db, action_db, liveaction_db): resolved_entry_point = self._get_entry_point_abs_path( action_db.pack, action_db.entry_point) context = getattr(liveaction_db, "context", dict()) user = context.get("user", cfg.CONF.system_user.user) config = None # Note: Right now configs are only supported by the Python runner actions if (runner_type_db.name == "python-script" or runner_type_db.runner_module == "python_runner"): LOG.debug("Loading config from pack for python runner.") config_loader = ContentPackConfigLoader(pack_name=action_db.pack, user=user) config = config_loader.get_config() runner = get_runner(name=runner_type_db.name, config=config) # TODO: Pass those arguments to the constructor instead of late # assignment, late assignment is awful runner.runner_type = runner_type_db runner.action = action_db runner.action_name = action_db.name runner.liveaction = liveaction_db runner.liveaction_id = str(liveaction_db.id) runner.execution = ActionExecution.get( liveaction__id=runner.liveaction_id) runner.execution_id = str(runner.execution.id) runner.entry_point = resolved_entry_point runner.context = context runner.callback = getattr(liveaction_db, "callback", dict()) runner.libs_dir_path = self._get_action_libs_abs_path( action_db.pack, action_db.entry_point) # For re-run, get the ActionExecutionDB in which the re-run is based on. rerun_ref_id = runner.context.get("re-run", {}).get("ref") runner.rerun_ex_ref = (ActionExecution.get( id=rerun_ref_id) if rerun_ref_id else None) return runner
def update_execution(liveaction_db, publish=True): execution = ActionExecution.get(liveaction__id=str(liveaction_db.id)) decomposed = _decompose_liveaction(liveaction_db) kw = {} for k, v in six.iteritems(decomposed): kw['set__' + k] = v if liveaction_db.status != execution.status: # Note: If the status changes we store this transition in the "log" attribute of action # execution kw['push__log'] = _create_execution_log_entry(liveaction_db.status) execution = ActionExecution.update(execution, publish=publish, **kw) return execution
def update_execution(liveaction_db, publish=True): execution = ActionExecution.get(liveaction__id=str(liveaction_db.id)) decomposed = _decompose_liveaction(liveaction_db) kw = {} for k, v in six.iteritems(decomposed): kw['set__' + k] = v if liveaction_db.status != execution.status: # Note: If the status changes we store this transition in the "log" attribute of action # execution kw['push__log'] = _create_execution_log_entry(liveaction_db.status) execution = ActionExecution.update(execution, publish=publish, **kw) return execution
def _fix_missing_action_execution_id(self): """ Auto-populate the action_execution_id in ActionExecutionSchedulingQueue if empty. """ for entry in ActionExecutionSchedulingQueue.query(action_execution_id__in=['', None]): execution_db = ActionExecution.get(liveaction__id=entry.liveaction_id) if not execution_db: continue msg = '[%s] Populating action_execution_id for item "%s".' LOG.info(msg, str(execution_db.id), str(entry.id)) entry.action_execution_id = str(execution_db.id) ActionExecutionSchedulingQueue.add_or_update(entry, publish=False)
def _get_runner(self, runner_type_db, action_db, liveaction_db): resolved_entry_point = self._get_entry_point_abs_path(action_db.pack, action_db.entry_point) context = getattr(liveaction_db, 'context', dict()) user = context.get('user', cfg.CONF.system_user.user) config = None # Note: Right now configs are only supported by the Python runner actions if (runner_type_db.name == 'python-script' or runner_type_db.runner_module == 'python_runner'): LOG.debug('Loading config from pack for python runner.') config_loader = ContentPackConfigLoader(pack_name=action_db.pack, user=user) config = config_loader.get_config() runner = get_runner( name=runner_type_db.name, config=config) # TODO: Pass those arguments to the constructor instead of late # assignment, late assignment is awful runner.runner_type = runner_type_db runner.action = action_db runner.action_name = action_db.name runner.liveaction = liveaction_db runner.liveaction_id = str(liveaction_db.id) runner.execution = ActionExecution.get(liveaction__id=runner.liveaction_id) runner.execution_id = str(runner.execution.id) runner.entry_point = resolved_entry_point runner.context = context runner.callback = getattr(liveaction_db, 'callback', dict()) runner.libs_dir_path = self._get_action_libs_abs_path(action_db.pack, action_db.entry_point) # For re-run, get the ActionExecutionDB in which the re-run is based on. rerun_ref_id = runner.context.get('re-run', {}).get('ref') runner.rerun_ex_ref = ActionExecution.get(id=rerun_ref_id) if rerun_ref_id else None return runner
def pause(self): # Identify the list of action executions that are workflows and cascade pause. for child_exec_id in self.execution.children: child_exec = ActionExecution.get(id=child_exec_id, raise_exception=True) if (child_exec.runner['name'] in action_constants.WORKFLOW_RUNNER_TYPES and child_exec.status == action_constants.LIVEACTION_STATUS_RUNNING): action_service.request_pause( LiveAction.get(id=child_exec.liveaction['id']), self.context.get('user', None)) return (action_constants.LIVEACTION_STATUS_PAUSING, self.liveaction.result, self.liveaction.context)
def _format_action_exec_result(self, action_node, liveaction_db, created_at, updated_at, error=None): """ Format ActionExecution result so it can be used in the final action result output. :rtype: ``dict`` """ if not isinstance(created_at, datetime.datetime): raise TypeError( f"The created_at is not a datetime object was({type(created_at)})." ) if not isinstance(updated_at, datetime.datetime): raise TypeError( f"The updated_at is not a datetime object was({type(updated_at)})." ) result = {} execution_db = None if liveaction_db: execution_db = ActionExecution.get( liveaction__id=str(liveaction_db.id)) result["id"] = action_node.name result["name"] = action_node.name result["execution_id"] = str(execution_db.id) if execution_db else None result["liveaction_id"] = str( liveaction_db.id) if liveaction_db else None result["workflow"] = None result["created_at"] = isotime.format(dt=created_at) result["updated_at"] = isotime.format(dt=updated_at) if error or not liveaction_db: result["state"] = action_constants.LIVEACTION_STATUS_FAILED else: result["state"] = liveaction_db.status if error: result["result"] = error else: result["result"] = liveaction_db.result return result
def get_parent_execution(execution_db): """Get the action execution for the parent workflow Useful for finding the parent workflow. Pass in any ActionExecutionDB instance, and this function will return the action execution of the parent workflow. :param execution_db: The ActionExecutionDB instance for which to find the parent. :rtype: ActionExecutionDB """ if not execution_db.parent: return None parent_execution_db = ActionExecution.get(id=execution_db.parent) return parent_execution_db
def _create_execution_queue_item_db_from_liveaction( self, liveaction, delay=None): """ Create ActionExecutionSchedulingQueueItemDB from live action. """ execution = ActionExecution.get(liveaction__id=str(liveaction.id)) execution_queue_item_db = ActionExecutionSchedulingQueueItemDB() execution_queue_item_db.action_execution_id = str(execution.id) execution_queue_item_db.liveaction_id = str(liveaction.id) execution_queue_item_db.original_start_timestamp = liveaction.start_timestamp execution_queue_item_db.scheduled_start_timestamp = date.append_milliseconds_to_time( liveaction.start_timestamp, delay or 0) execution_queue_item_db.delay = delay return execution_queue_item_db
def get_parent_execution(execution_db): """Get the action execution for the parent workflow Useful for finding the parent workflow. Pass in any ActionExecutionDB instance, and this function will return the action execution of the parent workflow. :param execution_db: The ActionExecutionDB instance for which to find the parent. :rtype: ActionExecutionDB """ if not execution_db.parent: return None parent_execution_db = ActionExecution.get(id=execution_db.parent) return parent_execution_db
def pause(self): # Identify the list of action executions that are workflows and cascade pause. for child_exec_id in self.execution.children: child_exec = ActionExecution.get(id=child_exec_id, raise_exception=True) if (child_exec.runner['name'] in action_constants.WORKFLOW_RUNNER_TYPES and child_exec.status == action_constants.LIVEACTION_STATUS_RUNNING): action_service.request_pause( LiveAction.get(id=child_exec.liveaction['id']), self.context.get('user', None) ) return ( action_constants.LIVEACTION_STATUS_PAUSING, self.liveaction.result, self.liveaction.context )
def get_root_execution(ac_ex_db): """Recursively ascends until the root action execution is found Useful for finding an original parent workflow. Pass in any ActionExecutionDB instance, and this function will eventually return the top-most action execution, even if the two are one and the same. :param ac_ex_db: The ActionExecutionDB instance for which to find the root parent. :rtype: ActionExecutionDB """ if not ac_ex_db.parent: return ac_ex_db parent_ac_ex_db = ActionExecution.get(id=ac_ex_db.parent) return get_root_execution(parent_ac_ex_db)
def _create_execution_queue_item_db_from_liveaction(self, liveaction, delay=None): """ Create ActionExecutionSchedulingQueueItemDB from live action. """ execution = ActionExecution.get(liveaction__id=str(liveaction.id)) execution_queue_item_db = ActionExecutionSchedulingQueueItemDB() execution_queue_item_db.action_execution_id = str(execution.id) execution_queue_item_db.liveaction_id = str(liveaction.id) execution_queue_item_db.original_start_timestamp = liveaction.start_timestamp execution_queue_item_db.scheduled_start_timestamp = date.append_milliseconds_to_time( liveaction.start_timestamp, delay or 0 ) execution_queue_item_db.delay = delay return execution_queue_item_db
def _cancel_action(self, liveaction_db): action_execution_db = ActionExecution.get(liveaction__id=str(liveaction_db.id)) extra = {'action_execution_db': action_execution_db, 'liveaction_db': liveaction_db} LOG.audit('Canceling action execution.', extra=extra) # the extra field will not be shown in non-audit logs so temporarily log at info. LOG.info('Dispatched {~}action_execution: %s / {~}live_action: %s with "%s" status.', action_execution_db.id, liveaction_db.id, liveaction_db.status) try: result = self.container.dispatch(liveaction_db) LOG.debug('Runner dispatch produced result: %s', result) except: _, ex, tb = sys.exc_info() extra['error'] = str(ex) LOG.info('Failed to cancel action execution %s.' % (liveaction_db.id), extra=extra) raise return result
def get_parent_liveaction(liveaction_db): """Get the liveaction for the parent workflow Useful for finding the parent workflow. Pass in any LiveActionDB instance, and this function will return the liveaction of the parent workflow. :param liveaction_db: The LiveActionDB instance for which to find the parent. :rtype: LiveActionDB """ parent = liveaction_db.context.get('parent') if not parent: return None parent_execution_db = ActionExecution.get(id=parent['execution_id']) parent_liveaction_db = LiveAction.get(id=parent_execution_db.liveaction['id']) return parent_liveaction_db
def is_children_active(liveaction_id): execution_db = ActionExecution.get(liveaction__id=str(liveaction_id)) if execution_db.runner['name'] not in action_constants.WORKFLOW_RUNNER_TYPES: return False children_execution_dbs = ActionExecution.query(parent=str(execution_db.id)) inactive_statuses = ( action_constants.LIVEACTION_COMPLETED_STATES + [action_constants.LIVEACTION_STATUS_PAUSED, action_constants.LIVEACTION_STATUS_PENDING] ) completed = [ child_exec_db.status in inactive_statuses for child_exec_db in children_execution_dbs ] return (not all(completed))
def request_cancellation(liveaction, requester): """ Request cancellation of an action execution. :return: (liveaction, execution) :rtype: tuple """ if liveaction.status == action_constants.LIVEACTION_STATUS_CANCELING: return liveaction if liveaction.status not in action_constants.CANCELABLE_STATES: raise Exception("Unable to cancel execution because it is already in a completed state.") result = {"message": "Action canceled by user.", "user": requester} update_status(liveaction, action_constants.LIVEACTION_STATUS_CANCELING, result=result) execution = ActionExecution.get(liveaction__id=str(liveaction.id)) return (liveaction, execution)
def _get_runner(self, runnertype_db, action_db, liveaction_db): runner = get_runner(runnertype_db.runner_module) resolved_entry_point = self._get_entry_point_abs_path(action_db.pack, action_db.entry_point) runner.container_service = RunnerContainerService() runner.action = action_db runner.action_name = action_db.name runner.liveaction = liveaction_db runner.liveaction_id = str(liveaction_db.id) runner.execution = ActionExecution.get(liveaction__id=runner.liveaction_id) runner.execution_id = str(runner.execution.id) runner.entry_point = resolved_entry_point runner.context = getattr(liveaction_db, 'context', dict()) runner.callback = getattr(liveaction_db, 'callback', dict()) runner.libs_dir_path = self._get_action_libs_abs_path(action_db.pack, action_db.entry_point) return runner
def create_execution_object(liveaction, publish=True): action_db = action_utils.get_action_by_ref(liveaction.action) runner = RunnerType.get_by_name(action_db.runner_type['name']) attrs = { 'action': vars(ActionAPI.from_model(action_db)), 'runner': vars(RunnerTypeAPI.from_model(runner)) } attrs.update(_decompose_liveaction(liveaction)) if 'rule' in liveaction.context: rule = reference.get_model_from_ref(Rule, liveaction.context.get('rule', {})) attrs['rule'] = vars(RuleAPI.from_model(rule)) if 'trigger_instance' in liveaction.context: trigger_instance_id = liveaction.context.get('trigger_instance', {}) trigger_instance_id = trigger_instance_id.get('id', None) trigger_instance = TriggerInstance.get_by_id(trigger_instance_id) trigger = reference.get_model_by_resource_ref(db_api=Trigger, ref=trigger_instance.trigger) trigger_type = reference.get_model_by_resource_ref(db_api=TriggerType, ref=trigger.type) trigger_instance = reference.get_model_from_ref( TriggerInstance, liveaction.context.get('trigger_instance', {})) attrs['trigger_instance'] = vars(TriggerInstanceAPI.from_model(trigger_instance)) attrs['trigger'] = vars(TriggerAPI.from_model(trigger)) attrs['trigger_type'] = vars(TriggerTypeAPI.from_model(trigger_type)) parent = ActionExecution.get(liveaction__id=liveaction.context.get('parent', '')) if parent: attrs['parent'] = str(parent.id) execution = ActionExecutionDB(**attrs) execution = ActionExecution.add_or_update(execution, publish=publish) if parent: if str(execution.id) not in parent.children: parent.children.append(str(execution.id)) ActionExecution.add_or_update(parent) return execution
def cancel(self): mistral_ctx = self.context.get('mistral', dict()) if not mistral_ctx.get('execution_id'): raise Exception('Unable to cancel because mistral execution_id is missing.') # Cancels the main workflow execution. Any non-workflow tasks that are still # running will be allowed to complete gracefully. self._client.executions.update(mistral_ctx.get('execution_id'), 'CANCELLED') # If workflow is executed under another parent workflow, cancel the corresponding # action execution for the task in the parent workflow. if 'parent' in getattr(self, 'context', {}) and mistral_ctx.get('action_execution_id'): mistral_action_ex_id = mistral_ctx.get('action_execution_id') self._client.action_executions.update(mistral_action_ex_id, 'CANCELLED') # Identify the list of action executions that are workflows and still running. for child_exec_id in self.execution.children: child_exec = ActionExecution.get(id=child_exec_id) if (child_exec.runner['name'] in action_constants.WORKFLOW_RUNNER_TYPES and child_exec.status in action_constants.LIVEACTION_CANCELABLE_STATES): action_service.request_cancellation( LiveAction.get(id=child_exec.liveaction['id']), self.context.get('user', None) ) status = ( action_constants.LIVEACTION_STATUS_CANCELING if action_service.is_children_active(self.liveaction.id) else action_constants.LIVEACTION_STATUS_CANCELED ) return ( status, self.liveaction.result, self.liveaction.context )
def update_status(liveaction_api, liveaction_db): status = liveaction_api.status result = getattr(liveaction_api, 'result', None) liveaction_db = action_service.update_status(liveaction_db, status, result) actionexecution_db = ActionExecution.get(liveaction__id=str(liveaction_db.id)) return (liveaction_db, actionexecution_db)
def _get_action_execution(self, **kwargs): return ActionExecution.get(**kwargs)
def test_over_threshold_delay_executions(self): # Ensure the concurrency policy is accurate. policy_db = Policy.get_by_ref('wolfpack.action-1.concurrency') self.assertGreater(policy_db.parameters['threshold'], 0) # Launch action executions until the expected threshold is reached. for i in range(0, policy_db.parameters['threshold']): parameters = {'actionstr': 'foo-' + str(i)} liveaction = LiveActionDB(action='wolfpack.action-1', parameters=parameters) action_service.request(liveaction) # Run the scheduler to schedule action executions. self._process_scheduling_queue() # Check the number of action executions in scheduled state. scheduled = [item for item in LiveAction.get_all() if item.status in SCHEDULED_STATES] self.assertEqual(len(scheduled), policy_db.parameters['threshold']) # Assert the correct number of published states and action executions. This is to avoid # duplicate executions caused by accidental publishing of state in the concurrency policies. # num_state_changes = len(scheduled) * len(['requested', 'scheduled', 'running']) expected_num_exec = len(scheduled) expected_num_pubs = expected_num_exec * 3 self.assertEqual(expected_num_pubs, LiveActionPublisher.publish_state.call_count) self.assertEqual(expected_num_exec, runner.MockActionRunner.run.call_count) # Execution is expected to be delayed since concurrency threshold is reached. liveaction = LiveActionDB(action='wolfpack.action-1', parameters={'actionstr': 'foo-last'}) liveaction, _ = action_service.request(liveaction) expected_num_pubs += 1 # Tally requested state. self.assertEqual(expected_num_pubs, LiveActionPublisher.publish_state.call_count) # Run the scheduler to schedule action executions. self._process_scheduling_queue() # Since states are being processed async, wait for the liveaction to go into delayed state. liveaction = self._wait_on_status(liveaction, action_constants.LIVEACTION_STATUS_DELAYED) expected_num_exec += 0 # This request will not be scheduled for execution. expected_num_pubs += 0 # The delayed status change should not be published. self.assertEqual(expected_num_pubs, LiveActionPublisher.publish_state.call_count) self.assertEqual(expected_num_exec, runner.MockActionRunner.run.call_count) # Mark one of the scheduled/running execution as completed. action_service.update_status( scheduled[0], action_constants.LIVEACTION_STATUS_SUCCEEDED, publish=True ) expected_num_pubs += 1 # Tally succeeded state. self.assertEqual(expected_num_pubs, LiveActionPublisher.publish_state.call_count) # Run the scheduler to schedule action executions. self._process_scheduling_queue() # Once capacity freed up, the delayed execution is published as scheduled. expected_num_exec += 1 # This request is expected to be executed. expected_num_pubs += 2 # Tally scheduled and running state. # Since states are being processed async, wait for the liveaction to be scheduled. liveaction = self._wait_on_statuses(liveaction, SCHEDULED_STATES) self.assertEqual(expected_num_pubs, LiveActionPublisher.publish_state.call_count) self.assertEqual(expected_num_exec, runner.MockActionRunner.run.call_count) # Check the status changes. execution = ActionExecution.get(liveaction__id=str(liveaction.id)) expected_status_changes = ['requested', 'delayed', 'requested', 'scheduled', 'running'] actual_status_changes = [entry['status'] for entry in execution.log] self.assertListEqual(actual_status_changes, expected_status_changes)