def test_handle_timer_fired(clock_decision_context: ClockDecisionContext, request_info, decider): attributes = TimerFiredEventAttributes() attributes.started_event_id = START_TIMER_ID clock_decision_context.handle_timer_fired(attributes) decider.handle_timer_closed.assert_called_once() args, kwargs = decider.handle_timer_closed.call_args_list[0] assert args[0] is attributes assert len(clock_decision_context.scheduled_timers) == 0 request_info.completion_handle.assert_called_once() args, kwargs = request_info.completion_handle.call_args_list[0] assert args[0] is None assert args[1] is None
def __post_init__(self): if not self.workflow_clock: self.workflow_clock = ClockDecisionContext(self.decider, self)
class DecisionContext: decider: ReplayDecider scheduled_activities: Dict[int, Future[bytes]] = field(default_factory=dict) workflow_clock: ClockDecisionContext = None current_run_id: str = None def __post_init__(self): if not self.workflow_clock: self.workflow_clock = ClockDecisionContext(self.decider, self) async def schedule_activity_task(self, parameters: ExecuteActivityParameters): attr = ScheduleActivityTaskDecisionAttributes() attr.activity_type = parameters.activity_type attr.input = parameters.input if parameters.heartbeat_timeout_seconds > 0: attr.heartbeat_timeout_seconds = parameters.heartbeat_timeout_seconds attr.schedule_to_close_timeout_seconds = parameters.schedule_to_close_timeout_seconds attr.schedule_to_start_timeout_seconds = parameters.schedule_to_start_timeout_seconds attr.start_to_close_timeout_seconds = parameters.start_to_close_timeout_seconds attr.activity_id = parameters.activity_id if not attr.activity_id: attr.activity_id = self.decider.get_and_increment_next_id() attr.task_list = TaskList() attr.task_list.name = parameters.task_list if parameters.retry_parameters: attr.retry_policy = parameters.retry_parameters.to_retry_policy() scheduled_event_id = self.decider.schedule_activity_task(schedule=attr) future = self.decider.event_loop.create_future() self.scheduled_activities[scheduled_event_id] = future try: await future except CancelledError as e: logger.debug("Coroutine cancelled (expected)") raise e except Exception as ex: pass ex = future.exception() if ex: activity_failure = ActivityFailureException( scheduled_event_id, parameters.activity_type.name, parameters.activity_id, serialize_exception(ex)) raise activity_failure assert future.done() raw_bytes = future.result() return json.loads(str(raw_bytes, "utf-8")) async def schedule_timer(self, seconds: int): future = self.decider.event_loop.create_future() def callback(ex: Exception): nonlocal future if ex: future.set_exception(ex) else: future.set_result("time-fired") self.decider.decision_context.create_timer(delay_seconds=seconds, callback=callback) await future assert future.done() exception = future.exception() if exception: raise exception return def handle_activity_task_completed(self, event: HistoryEvent): attr = event.activity_task_completed_event_attributes if self.decider.handle_activity_task_closed(attr.scheduled_event_id): future = self.scheduled_activities.get(attr.scheduled_event_id) if future: self.scheduled_activities.pop(attr.scheduled_event_id) future.set_result(attr.result) else: raise NonDeterministicWorkflowException( f"Trying to complete activity event {attr.scheduled_event_id} that is not in scheduled_activities" ) def handle_activity_task_failed(self, event: HistoryEvent): attr = event.activity_task_failed_event_attributes if self.decider.handle_activity_task_closed(attr.scheduled_event_id): future = self.scheduled_activities.get(attr.scheduled_event_id) if future: self.scheduled_activities.pop(attr.scheduled_event_id) # TODO: attr.reason - what should we do with it? ex = deserialize_exception(attr.details) future.set_exception(ex) else: raise NonDeterministicWorkflowException( f"Trying to complete activity event {attr.scheduled_event_id} that is not in scheduled_activities" ) def handle_activity_task_timed_out(self, event: HistoryEvent): attr = event.activity_task_timed_out_event_attributes if self.decider.handle_activity_task_closed(attr.scheduled_event_id): future = self.scheduled_activities.get(attr.scheduled_event_id) if future: self.scheduled_activities.pop(attr.scheduled_event_id) ex = ActivityTaskTimeoutException(event.event_id, attr.timeout_type, attr.details) future.set_exception(ex) else: raise NonDeterministicWorkflowException( f"Trying to complete activity event {attr.scheduled_event_id} that is not in scheduled_activities" ) def create_timer(self, delay_seconds: int, callback: Callable): return self.workflow_clock.create_timer(delay_seconds, callback) def set_replay_current_time_milliseconds( self, replay_current_time_milliseconds: int): if replay_current_time_milliseconds < self.workflow_clock.current_time_millis( ): raise Exception("workflow clock moved back") self.workflow_clock.set_replay_current_time_milliseconds( replay_current_time_milliseconds) def current_time_millis(self): return self.workflow_clock.current_time_millis() def set_replaying(self, replaying: bool): self.workflow_clock.set_replaying(replaying) def is_replaying(self): return self.workflow_clock.is_replaying() def handle_timer_fired(self, attributes: TimerFiredEventAttributes): self.workflow_clock.handle_timer_fired(attributes) def handle_timer_canceled(self, event: HistoryEvent): self.workflow_clock.handle_timer_canceled(event) def set_current_run_id(self, run_id: str): self.current_run_id = run_id def random_uuid(self) -> uuid.UUID: return uuid.uuid3(uuid.UUID(self.current_run_id), str(self.decider.get_and_increment_next_id())) def new_random(self) -> random.Random: random_uuid = self.random_uuid() lsb = random_uuid.bytes[:8] generator = random.Random() generator.seed(lsb, version=2) return generator def record_marker(self, marker_name: str, header: Header, details: bytes): marker = RecordMarkerDecisionAttributes() marker.marker_name = marker_name marker.header = header marker.details = details decision = Decision() decision.decision_type = DecisionType.RecordMarker decision.record_marker_decision_attributes = marker next_decision_event_id = self.decider.next_decision_event_id decision_id = DecisionId(DecisionTarget.MARKER, next_decision_event_id) self.decider.add_decision( decision_id, MarkerDecisionStateMachine(id=decision_id, decision=decision)) def get_version(self, change_id: str, min_supported: int, max_supported: int) -> int: return self.workflow_clock.get_version(change_id, min_supported, max_supported) def get_logger(self, name) -> logging.Logger: replay_aware_logger = logging.getLogger(name) make_replay_aware(replay_aware_logger) return replay_aware_logger
def clock_decision_context(decider): context = ClockDecisionContext(decider=decider) context.set_replay_current_time_milliseconds(REPLAY_CURRENT_TIME_MS) return context
class DecisionContext: decider: ReplayDecider scheduled_activities: Dict[int, Future[bytes]] = field(default_factory=dict) workflow_clock: ClockDecisionContext = None current_run_id: str = None def __post_init__(self): if not self.workflow_clock: self.workflow_clock = ClockDecisionContext(self.decider) async def schedule_activity_task(self, parameters: ExecuteActivityParameters): attr = ScheduleActivityTaskDecisionAttributes() attr.activity_type = parameters.activity_type attr.input = parameters.input if parameters.heartbeat_timeout_seconds > 0: attr.heartbeat_timeout_seconds = parameters.heartbeat_timeout_seconds attr.schedule_to_close_timeout_seconds = parameters.schedule_to_close_timeout_seconds attr.schedule_to_start_timeout_seconds = parameters.schedule_to_start_timeout_seconds attr.start_to_close_timeout_seconds = parameters.start_to_close_timeout_seconds attr.activity_id = parameters.activity_id if not attr.activity_id: attr.activity_id = self.decider.get_and_increment_next_id() attr.task_list = TaskList() attr.task_list.name = parameters.task_list # PORT: RetryParameters retryParameters = parameters.getRetryParameters(); # PORT: if (retryParameters != null) { # PORT: attributes.setRetryPolicy(retryParameters.toRetryPolicy()); # PORT: } scheduled_event_id = self.decider.schedule_activity_task(schedule=attr) future = self.decider.event_loop.create_future() self.scheduled_activities[scheduled_event_id] = future await future assert future.done() exception = future.exception() if exception: raise exception raw_bytes = future.result() return json.loads(str(raw_bytes, "utf-8")) async def schedule_timer(self, seconds: int): future = self.decider.event_loop.create_future() def callback(ex: Exception): nonlocal future if ex: future.set_exception(ex) else: future.set_result("time-fired") self.decider.decision_context.create_timer(delay_seconds=seconds, callback=callback) await future assert future.done() exception = future.exception() if exception: raise exception return def handle_activity_task_completed(self, event: HistoryEvent): attr = event.activity_task_completed_event_attributes if self.decider.handle_activity_task_closed(attr.scheduled_event_id): future = self.scheduled_activities.get(attr.scheduled_event_id) if future: self.scheduled_activities.pop(attr.scheduled_event_id) future.set_result(attr.result) else: raise NonDeterministicWorkflowException( f"Trying to complete activity event {attr.scheduled_event_id} that is not in scheduled_activities") def handle_activity_task_failed(self, event: HistoryEvent): attr = event.activity_task_failed_event_attributes if self.decider.handle_activity_task_closed(attr.scheduled_event_id): future = self.scheduled_activities.get(attr.scheduled_event_id) if future: self.scheduled_activities.pop(attr.scheduled_event_id) ex = ActivityTaskFailedException(attr.reason, attr.details) future.set_exception(ex) else: raise NonDeterministicWorkflowException( f"Trying to complete activity event {attr.scheduled_event_id} that is not in scheduled_activities") def handle_activity_task_timed_out(self, event: HistoryEvent): attr = event.activity_task_timed_out_event_attributes if self.decider.handle_activity_task_closed(attr.scheduled_event_id): future = self.scheduled_activities.get(attr.scheduled_event_id) if future: self.scheduled_activities.pop(attr.scheduled_event_id) ex = ActivityTaskTimeoutException(event.event_id, attr.timeout_type, attr.details) future.set_exception(ex) else: raise NonDeterministicWorkflowException( f"Trying to complete activity event {attr.scheduled_event_id} that is not in scheduled_activities") def create_timer(self, delay_seconds: int, callback: Callable): return self.workflow_clock.create_timer(delay_seconds, callback) def set_replay_current_time_milliseconds(self, replay_current_time_milliseconds: int): if replay_current_time_milliseconds < self.workflow_clock.current_time_millis(): raise Exception("workflow clock moved back") self.workflow_clock.set_replay_current_time_milliseconds(replay_current_time_milliseconds) def current_time_millis(self): return self.workflow_clock.current_time_millis() def set_replaying(self, replaying: bool): self.workflow_clock.set_replaying(replaying) def is_replaying(self): return self.workflow_clock.is_replaying() def handle_timer_fired(self, attributes: TimerFiredEventAttributes): self.workflow_clock.handle_timer_fired(attributes) def handle_timer_canceled(self, event: HistoryEvent): self.workflow_clock.handle_timer_canceled(event) def set_current_run_id(self, run_id: str): self.current_run_id = run_id def random_uuid(self) -> uuid.UUID: return uuid.uuid3(uuid.UUID(self.current_run_id), str(self.decider.get_and_increment_next_id())) def new_random(self) -> random.Random: random_uuid = self.random_uuid() lsb = random_uuid.bytes[:8] generator = random.Random() generator.seed(lsb, version=2) return generator