async def test(): tmpl = TEMPLATES['task_timeout'] wflow = Workflow(WorkflowTemplate.from_dict(tmpl)) wflow.run({'initial': 'data'}) # The workflow is OK await wflow self.assertEqual(FutureState.get(wflow), FutureState.finished) # The task has timed out task = wflow._tasks_by_id.get('1') with self.assertRaises(asyncio.CancelledError): task.exception() self.assertEqual(FutureState.get(task), FutureState.timeout)
async def test(): tmpl = TEMPLATES['ok'] wflow = Workflow(WorkflowTemplate.from_dict(tmpl)) wflow.run({'initial': 'data'}) await wflow # These tasks have finished for tid in tmpl['graph'].keys(): task = wflow._tasks_by_id.get(tid) self.assertTrue(task.done()) self.assertEqual(FutureState.get(task), FutureState.finished) # The workflow finished properly self.assertTrue(wflow.done()) self.assertEqual(FutureState.get(wflow), FutureState.finished)
def report(self): """ Creates and returns a complete execution report, including workflow and tasks templates and execution details. """ report = self._template.as_dict() report['exec'] = { 'id': self.uid, 'start': self._start, 'end': self._end, 'state': FutureState.get(self).value } # Update task descriptions to add info about their execution. for task_dict in report['tasks']: try: task = self._tasks_by_id[task_dict['id']] except KeyError: task_dict['exec'] = None continue task_dict['exec'] = task.as_dict() # If the task is linked to a task holder, try to use its own report if hasattr(task.holder, 'report'): try: task_dict['exec']['reporting'] = task.holder.report() except Exception as exc: # Unexpected error from task reporting. log.error('Exception on task reporting: %s', exc) self._internal_exc = exc self._try_mark_done() return report
def report(self): """ Creates and returns a complete execution report, including workflow and tasks templates and execution details. """ report = self._template.as_dict() report['exec'] = { 'id': self.uid, 'start': self._start, 'end': self._end, 'state': FutureState.get(self).value } # Update task descriptions to add info about their execution. for task_dict in report['tasks']: try: task = self._tasks_by_id[task_dict['id']] except KeyError: task_dict['exec'] = None continue task_dict['exec'] = task.as_dict() # If the task is linked to a task holder, try to use its own report try: task_dict['exec']['reporting'] = task.holder.report() except AttributeError: pass return report
async def test(): tmpl = TEMPLATES['workflow_cancel'] wflow = Workflow(WorkflowTemplate.from_dict(tmpl)) wflow.run({'initial': 'data'}) # Workflow is cancelled with self.assertRaises(asyncio.CancelledError): await wflow self.assertEqual(FutureState.get(wflow), FutureState.cancelled) # This task was cancelled task = wflow._tasks_by_id.get('cancel') with self.assertRaises(asyncio.CancelledError): task.exception() self.assertEqual(FutureState.get(task), FutureState.cancelled) # These tasks were never started for tid in ('2', '3', '4'): task = wflow._tasks_by_id.get(tid) self.assertIs(task, None)
def browse(entry, parent=None): """ Dive into the dag to: - ensure executed task contexts are restored. - find tasks that need to be executed. """ t_template = next( t for t in report['tasks'] if t['id'] == entry.uid ) t_report = t_template.get('exec') if not t_report: if not parent: raise RescueError(self.uid, 'root task never been started') # No execution report found, the task needs to be executed resume.append((entry, None, parent)) return t_state = FutureState(t_report['state']) if not t_state.done(): # Pending, suspended or cancelled tasks need to be executed resume.append((entry, t_template, parent)) return if t_state.done(): # We won't manually create an asyncio task since it won't be # run (the task is 'done'). Yet, we need a memory print of its # execution (mainly for reporting purposes). t_shadow = type('TukioTaskShadow', (object,), { 'done': lambda: True, 'cancelled': lambda: False, '_exception': None, 'as_dict': lambda: t_report }) # Add this task to the tracking sets self.tasks.add(t_shadow) self._done_tasks.add(t_shadow) self._tasks_by_id[entry.uid] = t_shadow # Recursive browing for t_next in self._template.dag.successors(entry): browse(t_next, t_template)
def browse(entry, parent=None): """ Dive into the dag to: - ensure executed task contexts are restored. - find tasks that need to be executed. """ t_template = next(t for t in report['tasks'] if t['id'] == entry.uid) t_report = t_template.get('exec') if not t_report: if not parent: raise RescueError(self.uid, 'root task never been started') # No execution report found, the task needs to be executed resume.append((entry, None, parent)) return t_state = FutureState(t_report['state']) if not t_state.done(): # Pending, suspended or cancelled tasks need to be executed resume.append((entry, t_template, parent)) return if t_state.done(): # We won't manually create an asyncio task since it won't be # run (the task is 'done'). Yet, we need a memory print of its # execution (mainly for reporting purposes). t_shadow = type( 'TukioTaskShadow', (object, ), { 'done': lambda: True, 'cancelled': lambda: False, '_exception': None, 'as_dict': lambda: t_report }) # Add this task to the tracking sets self.tasks.add(t_shadow) self._done_tasks.add(t_shadow) self._tasks_by_id[entry.uid] = t_shadow # Recursive browing for t_next in self._template.dag.successors(entry): browse(t_next, t_template)
def as_dict(self): """ Returns the execution informations of this task. """ return { 'id': self.uid, 'start': self._start, 'end': self._end, 'state': FutureState.get(self).value, 'inputs': self._inputs, 'outputs': self._outputs }
async def test(): tmpl = TEMPLATES['crash_test'] # Test crash at task __init__ wflow = Workflow(WorkflowTemplate.from_dict(tmpl)) wflow.run({'initial': 'data'}) await wflow # These tasks have finished for tid in ('1', '2'): task = wflow._tasks_by_id.get(tid) self.assertTrue(task.done()) self.assertEqual(FutureState.get(task), FutureState.finished) # These tasks were never started for tid in ('crash', 'wont_run'): task = wflow._tasks_by_id.get(tid) self.assertIs(task, None) # The workflow finished properly self.assertTrue(wflow.done()) self.assertEqual(FutureState.get(wflow), FutureState.finished) # Test crash inside a task tmpl['tasks'][0]['config'] = {'init_ok': None} wflow = Workflow(WorkflowTemplate.from_dict(tmpl)) wflow.run({'initial': 'data'}) await wflow # These tasks have finished for tid in ('1', '2'): task = wflow._tasks_by_id.get(tid) self.assertTrue(task.done()) self.assertEqual(FutureState.get(task), FutureState.finished) # This task crashed during execution task = wflow._tasks_by_id.get('crash') self.assertTrue(task.done()) self.assertEqual(FutureState.get(task), FutureState.exception) # This task was never started task = wflow._tasks_by_id.get('wont_run') self.assertIs(task, None) # The workflow finished properly self.assertTrue(wflow.done()) self.assertEqual(FutureState.get(wflow), FutureState.finished)
def resume(self): """ Resume the execution of this workflow allows new tasks to start. """ if self._committed.is_set(): log.error('Cannot resume a workflow that has not been suspended') return self._committed.set() # Next tasks are done waiting for '_committed' asyncio event # The 'suspended' ones needs to be re-executed for task in self._done_tasks: if FutureState.get(task) is not FutureState.suspended: continue event = Event(task.inputs, source=task.event_source) self._new_task(task.template, event) self._dispatch_exec_event(WorkflowExecState.resume) log.info('workflow %s has been resumed', self)
def resume(self): """ Resume the execution of this workflow allows new tasks to start. """ if self._committed.is_set(): log.error('Cannot resume a workflow that has not been suspended') return self._committed.set() # Next tasks are done waiting for '_committed' asyncio event # The 'suspended' ones needs to be re-executed for task in self._done_tasks: if FutureState.get(task) is not FutureState.suspended: continue event = Event(task.inputs, source=task.event_source) self._new_task(task.template, event) self._dispatch_exec_event(WorkflowExecState.RESUME) log.info('workflow %s has been resumed', self)
async def get(self, request): """ Filters: * `root` return only the root workflows * `full` return the full graph and details of all workflows * :warning: can be a huge amount of data * `since` return the workflows since this date * `state` return the workflows on this FutureState * `offset` return the worflows from this offset * `limit` return this amount of workflows * `order` order results following the Ordering enum values * `search` search templates with specific title """ # Filter on start date since = request.GET.get('since') if since: try: since = from_isoformat(since) except ValueError: return Response( status=400, body={'error': "Could not parse date '{}'".format(since)}) # Filter on state value state = request.GET.get('state') if state: try: state = FutureState(state) except ValueError: return Response( status=400, body={'error': "Unknown state '{}'".format(state)}) # Skip first items offset = request.GET.get('offset') if offset: try: offset = int(offset) except ValueError: return Response(status=400, body={'error': 'Offset must be an int'}) # Limit max result limit = request.GET.get('limit') if limit: try: limit = int(limit) except ValueError: return Response(status=400, body={'error': 'Limit must be an int'}) order = request.GET.get('ordering') if order: try: order = Ordering[order].value except KeyError: return Response(status=400, body={ 'error': 'Ordering must be in {}'.format( Ordering.keys()) }) try: count, history = await self.nyuki.storage.instances.get( root=(request.GET.get('root') == '1'), full=(request.GET.get('full') == '1'), search=request.GET.get('search'), order=order, offset=offset, limit=limit, since=since, state=state, ) except AutoReconnect: return Response(status=503) data = {'count': count, 'data': history} return Response(data)