Squashed 'SpiffWorkflow/' changes from ffb16867..841bd630

841bd630 Merge pull request #273 from sartography/bugfix/catch-timers-in-event-gateways
103c70d0 hacks to handle timer events like regular events

git-subtree-dir: SpiffWorkflow
git-subtree-split: 841bd63017bb1d92858456393f144b4e5b23c994
This commit is contained in:
jasquat 2022-12-16 13:23:00 -05:00
parent 71a2a3ec0e
commit df6e065606
6 changed files with 63 additions and 17 deletions

View File

@ -81,17 +81,18 @@ class EventDefinitionParser(TaskParser):
"""Parse the timerEventDefinition node and return an instance of TimerEventDefinition."""
try:
label = self.node.get('name', self.node.get('id'))
time_date = first(self.xpath('.//bpmn:timeDate'))
if time_date is not None:
return TimerEventDefinition(self.node.get('name'), time_date.text)
return TimerEventDefinition(label, time_date.text)
time_duration = first(self.xpath('.//bpmn:timeDuration'))
if time_duration is not None:
return TimerEventDefinition(self.node.get('name'), time_duration.text)
return TimerEventDefinition(label, time_duration.text)
time_cycle = first(self.xpath('.//bpmn:timeCycle'))
if time_cycle is not None:
return CycleTimerEventDefinition(self.node.get('name'), time_cycle.text)
return CycleTimerEventDefinition(label, time_cycle.text)
raise ValidationException("Unknown Time Specification", node=self.node, filename=self.filename)
except Exception as e:
raise ValidationException("Time Specification Error. " + str(e), node=self.node, filename=self.filename)

View File

@ -156,6 +156,9 @@ class EventBasedGateway(CatchingEvent):
def spec_type(self):
return 'Event Based Gateway'
def _predict_hook(self, my_task):
my_task._sync_children(self.outputs, state=TaskState.MAYBE)
def _on_complete_hook(self, my_task):
for child in my_task.children:
if not child.task_spec.event_definition.has_fired(child):

View File

@ -20,6 +20,7 @@
import datetime
from copy import deepcopy
from SpiffWorkflow.task import TaskState
class EventDefinition(object):
"""
@ -307,6 +308,11 @@ class TimerEventDefinition(EventDefinition):
The Timer is considered to have fired if the evaluated dateTime
expression is before datetime.datetime.now()
"""
if my_task.internal_data.get('event_fired'):
# If we manually send this event, this will be set
return True
dt = my_task.workflow.script_engine.evaluate(my_task, self.dateTime)
if isinstance(dt,datetime.timedelta):
if my_task._get_internal_data('start_time',None) is not None:
@ -330,6 +336,9 @@ class TimerEventDefinition(EventDefinition):
now = datetime.date.today()
return now > dt
def __eq__(self, other):
return self.__class__.__name__ == other.__class__.__name__ and self.label == other.label
def serialize(self):
retdict = super(TimerEventDefinition, self).serialize()
retdict['label'] = self.label
@ -363,6 +372,10 @@ class CycleTimerEventDefinition(EventDefinition):
# We will fire this timer whenever a cycle completes
# The task itself will manage counting how many times it fires
if my_task.internal_data.get('event_fired'):
# If we manually send this event, this will be set
return True
repeat, delta = my_task.workflow.script_engine.evaluate(my_task, self.cycle_definition)
# This is the first time we've entered this event
@ -393,6 +406,9 @@ class CycleTimerEventDefinition(EventDefinition):
my_task.internal_data['start_time'] = None
super(CycleTimerEventDefinition, self).reset(my_task)
def __eq__(self, other):
return self.__class__.__name__ == other.__class__.__name__ and self.label == other.label
def serialize(self):
retdict = super(CycleTimerEventDefinition, self).serialize()
retdict['label'] = self.label
@ -411,19 +427,27 @@ class MultipleEventDefinition(EventDefinition):
def event_type(self):
return 'Multiple'
def catch(self, my_task, event_definition=None):
event_definition.catch(my_task, event_definition)
def has_fired(self, my_task):
seen_events = my_task.internal_data.get('seen_events', [])
for event in self.event_definitions:
if isinstance(event, (TimerEventDefinition, CycleTimerEventDefinition)):
child = [c for c in my_task.children if c.task_spec.event_definition == event]
child[0].task_spec._update_hook(child[0])
child[0]._set_state(TaskState.MAYBE)
if event.has_fired(my_task):
seen_events.append(event)
if self.parallel:
# Parallel multiple need to match all events
seen_events = my_task.internal_data.get('seen_events', []) + [event_definition]
my_task._set_internal_data(seen_events=seen_events)
if all(event in seen_events for event in self.event_definitions):
my_task._set_internal_data(event_fired=True)
else:
my_task._set_internal_data(event_fired=False)
return all(event in seen_events for event in self.event_definitions)
else:
# Otherwise, matching one is sufficient
my_task._set_internal_data(event_fired=True)
return len(seen_events) > 0
def catch(self, my_task, event_definition=None):
event_definition.catch(my_task, event_definition)
seen_events = my_task.internal_data.get('seen_events', []) + [event_definition]
my_task._set_internal_data(seen_events=seen_events)
def reset(self, my_task):
my_task.internal_data.pop('seen_events', None)

View File

@ -54,7 +54,7 @@ class CatchingEvent(Simple, BpmnSpecMixin):
my_task._ready()
super(CatchingEvent, self)._update_hook(my_task)
def _on_ready(self, my_task):
def _on_ready_hook(self, my_task):
# None events don't propogate, so as soon as we're ready, we fire our event
if isinstance(self.event_definition, NoneEventDefinition):
@ -63,7 +63,7 @@ class CatchingEvent(Simple, BpmnSpecMixin):
# If we have not seen the event we're waiting for, enter the waiting state
if not self.event_definition.has_fired(my_task):
my_task._set_state(TaskState.WAITING)
super(CatchingEvent, self)._on_ready(my_task)
super(CatchingEvent, self)._on_ready_hook(my_task)
def _on_complete_hook(self, my_task):

View File

@ -3,7 +3,7 @@ from functools import partial
from SpiffWorkflow.bpmn.serializer.bpmn_converters import BpmnTaskSpecConverter
from SpiffWorkflow.bpmn.specs.events.StartEvent import StartEvent
from SpiffWorkflow.bpmn.specs.events.EndEvent import EndEvent
from SpiffWorkflow.bpmn.specs.events.IntermediateEvent import IntermediateThrowEvent, IntermediateCatchEvent, BoundaryEvent
from SpiffWorkflow.bpmn.specs.events.IntermediateEvent import IntermediateThrowEvent, IntermediateCatchEvent, BoundaryEvent, EventBasedGateway
from SpiffWorkflow.spiff.specs.none_task import NoneTask
from SpiffWorkflow.spiff.specs.manual_task import ManualTask
from SpiffWorkflow.spiff.specs.user_task import UserTask
@ -164,3 +164,7 @@ class ReceiveTaskConverter(SpiffEventConverter):
dct['prescript'] = spec.prescript
dct['postscript'] = spec.postscript
return dct
class EventBasedGatewayConverter(SpiffEventConverter):
def __init__(self, data_converter=None, typename=None):
super().__init__(EventBasedGateway, data_converter, typename)

View File

@ -19,7 +19,7 @@ class EventBsedGatewayTest(BpmnWorkflowTestCase):
def testEventBasedGatewaySaveRestore(self):
self.actual_test(True)
def actual_test(self, save_restore=False):
self.workflow.do_engine_steps()
@ -36,6 +36,20 @@ class EventBsedGatewayTest(BpmnWorkflowTestCase):
self.assertEqual(self.workflow.get_tasks_from_spec_name('message_2_event')[0].state, TaskState.CANCELLED)
self.assertEqual(self.workflow.get_tasks_from_spec_name('timer_event')[0].state, TaskState.CANCELLED)
def testTimeout(self):
self.workflow.do_engine_steps()
waiting_tasks = self.workflow.get_waiting_tasks()
self.assertEqual(len(waiting_tasks), 1)
timer_event = waiting_tasks[0].task_spec.event_definition.event_definitions[-1]
self.workflow.catch(timer_event)
self.workflow.refresh_waiting_tasks()
self.workflow.do_engine_steps()
self.assertEqual(self.workflow.is_completed(), True)
self.assertEqual(self.workflow.get_tasks_from_spec_name('message_1_event')[0].state, TaskState.CANCELLED)
self.assertEqual(self.workflow.get_tasks_from_spec_name('message_2_event')[0].state, TaskState.CANCELLED)
self.assertEqual(self.workflow.get_tasks_from_spec_name('timer_event')[0].state, TaskState.COMPLETED)
def testMultipleStart(self):
spec, subprocess = self.load_workflow_spec('multiple-start-parallel.bpmn', 'main')
workflow = BpmnWorkflow(spec)