def test_reset_outputs(before, after, outputs): """Test that outputs are reset correctly on state changes.""" tdef = TaskDef('foo', {}, 'live', '123') orig_status, orig_is_held = before new_status, new_is_held = after tstate = TaskState(tdef, '123', orig_status, orig_is_held) assert tstate.outputs.get_completed() == [] tstate.reset(status=new_status, is_held=new_is_held) assert tstate.outputs.get_completed() == outputs
def test_reset(state, is_held, should_reset): """Test that tasks do or don't have their state changed.""" tdef = TaskDef('foo', {}, 'live', '123', '123') # create task state: # * status: waiting # * is_held: true tstate = TaskState(tdef, '123', TASK_STATUS_WAITING, True) assert tstate.reset(state, is_held) == should_reset if is_held is not None: assert tstate.is_held == is_held if state is not None: assert tstate.status == state
class TaskProxy: """Represent an instance of a cycling task in a running workflow. Attributes: .clock_trigger_time: Clock trigger time in seconds since epoch. (Used for both old-style clock triggers and wall_clock xtrigger). .expire_time: Time in seconds since epoch when this task is considered expired. .identity: Task ID in POINT/NAME syntax. .tokens: Task ID tokens. .is_late: Is the task late? .is_manual_submit: Is the latest job submission due to a manual trigger? .job_vacated: Is the latest job pre-empted (or vacated)? .jobs: A list of job ids associated with the task proxy. .local_job_file_path: Path on workflow host to the latest job script for the task. .late_time: Time in seconds since epoch, beyond which the task is considered late if it is never active. .non_unique_events (collections.Counter): Count non-unique events (e.g. critical, warning, custom). .point: Cycle point of the task. .point_as_seconds: Cycle point as seconds since epoch. .poll_timer: Schedule for polling submitted or running jobs. .reload_successor: The task proxy object that replaces the current instance on reload. This attribute provides a useful link to the latest replacement instance while the current object may still be referenced by a job manipulation command. .submit_num: Number of times the task has attempted job submission. .summary (dict): job_runner_name (str): Name of job runner where latest job is submitted. description (str): Same as the .tdef.rtconfig['meta']['description'] attribute. execution_time_limit (float): Execution time limit of latest job. finished_time (float): Latest job exit time. finished_time_string (str): Latest job exit time as string. platforms_used (dict): Jobs' platform by submit number. label (str): The .point attribute as string. logfiles (list): List of names of (extra) known job log files. name (str): Same as the .tdef.name attribute. started_time (float): Latest job execution start time. started_time_string (str): Latest job execution start time as string. submit_method_id (str): Latest ID of job in job runner. submit_num (int): Same as the .submit_num attribute. submitted_time (float): Latest job submission time. submitted_time_string (str): Latest job submission time as string. title (str): Same as the .tdef.rtconfig['meta']['title'] attribute. .state: Object representing the state of this task. .platform: Dict containing info for platform where latest job is submitted. .tdef: The definition object of this task. .timeout: Timeout value in seconds since epoch for latest job submission/execution. .try_timers: Retry schedules as cylc.flow.task_action_timer.TaskActionTimer objects. .graph_children (dict) graph children: {msg: [(name, point), ...]} .flow_nums: flow_nums .waiting_on_job_prep: task waiting on job prep Args: tdef: The definition object of this task. start_point: Start point to calculate the task's cycle point on start-up or the cycle point for subsequent tasks. flow_nums: Which flow within the scheduler this task belongs to. status: Task state string. is_held: True if the task is held, else False. submit_num: Number of times the task has attempted job submission. is_late: Is the task late? """ # Memory optimization - constrain possible attributes to this list. __slots__ = [ 'clock_trigger_time', 'expire_time', 'identity', 'is_late', 'is_manual_submit', 'job_vacated', 'jobs', 'late_time', 'local_job_file_path', 'non_unique_events', 'point', 'point_as_seconds', 'poll_timer', 'reload_successor', 'submit_num', 'tdef', 'state', 'summary', 'flow_nums', 'graph_children', 'platform', 'timeout', 'tokens', 'try_timers', 'waiting_on_job_prep', ] def __init__( self, tdef: 'TaskDef', start_point: 'PointBase', flow_nums: Optional[Set[int]] = None, status: str = TASK_STATUS_WAITING, is_held: bool = False, submit_num: int = 0, is_late: bool = False, ) -> None: self.tdef = tdef if submit_num is None: submit_num = 0 self.submit_num = submit_num self.jobs: List[dict] = [] if flow_nums is None: self.flow_nums = set() else: self.flow_nums = flow_nums self.point = start_point self.tokens = Tokens( # TODO: make these absolute? cycle=str(self.point), task=self.tdef.name, ) self.identity = self.tokens.relative_id self.reload_successor: Optional['TaskProxy'] = None self.point_as_seconds: Optional[int] = None self.is_manual_submit = False self.summary: Dict[str, Any] = { 'submitted_time': None, 'submitted_time_string': None, 'started_time': None, 'started_time_string': None, 'finished_time': None, 'finished_time_string': None, 'logfiles': [], 'platforms_used': {}, 'execution_time_limit': None, 'job_runner_name': None, 'submit_method_id': None, 'flow_nums': set() } self.local_job_file_path: Optional[str] = None self.platform = get_platform() self.job_vacated = False self.poll_timer: Optional['TaskActionTimer'] = None self.timeout: Optional[float] = None self.try_timers: Dict[str, 'TaskActionTimer'] = {} self.non_unique_events = Counter() # type: ignore # TODO: figure out self.clock_trigger_time: Optional[float] = None self.expire_time: Optional[float] = None self.late_time: Optional[float] = None self.is_late = is_late self.waiting_on_job_prep = True self.state = TaskState(tdef, self.point, status, is_held) # Determine graph children of this task (for spawning). self.graph_children = generate_graph_children(tdef, self.point) def __repr__(self) -> str: return f"<{self.__class__.__name__} '{self.tokens}'>" def __str__(self) -> str: """Stringify with tokens, state, submit_num, and flow_nums.""" return (f"{self.identity} " f"{self.state} " f"job:{self.submit_num:02d}" f" flows:{','.join(str(i) for i in self.flow_nums) or 'none'}") def copy_to_reload_successor(self, reload_successor): """Copy attributes to successor on reload of this task proxy.""" self.reload_successor = reload_successor reload_successor.submit_num = self.submit_num reload_successor.is_manual_submit = self.is_manual_submit reload_successor.summary = self.summary reload_successor.local_job_file_path = self.local_job_file_path reload_successor.try_timers = self.try_timers reload_successor.platform = self.platform reload_successor.job_vacated = self.job_vacated reload_successor.poll_timer = self.poll_timer reload_successor.timeout = self.timeout reload_successor.state.outputs = self.state.outputs reload_successor.state.is_held = self.state.is_held reload_successor.state.is_runahead = self.state.is_runahead reload_successor.state.is_updated = self.state.is_updated reload_successor.state.prerequisites = self.state.prerequisites reload_successor.jobs = self.jobs @staticmethod def get_offset_as_seconds(offset): """Return an ISO interval as seconds.""" iso_offset = interval_parse(str(offset)) return int(iso_offset.get_seconds()) def get_late_time(self): """Compute and store late time as seconds since epoch.""" if self.late_time is None: if self.tdef.rtconfig['events']['late offset']: self.late_time = (self.get_point_as_seconds() + self.tdef.rtconfig['events']['late offset']) else: # Not used, but allow skip of the above "is None" test self.late_time = 0 return self.late_time def get_point_as_seconds(self): """Compute and store my cycle point as seconds since epoch.""" if self.point_as_seconds is None: iso_timepoint = point_parse(str(self.point)) self.point_as_seconds = int( iso_timepoint.get('seconds_since_unix_epoch')) if iso_timepoint.time_zone.unknown: utc_offset_hours, utc_offset_minutes = (get_local_time_zone()) utc_offset_in_seconds = (3600 * utc_offset_hours + 60 * utc_offset_minutes) self.point_as_seconds += utc_offset_in_seconds return self.point_as_seconds def get_clock_trigger_time(self, offset_str): """Compute, cache, and return trigger time relative to cycle point. Args: offset_str: ISO8601Interval string, e.g. "PT2M". Can be None for zero offset. Returns: Absolute trigger time in seconds since Unix epoch. """ if self.clock_trigger_time is None: if offset_str is None: trigger_time = self.point else: trigger_time = self.point + ISO8601Interval(offset_str) self.clock_trigger_time = int( point_parse(str(trigger_time)).get('seconds_since_unix_epoch')) return self.clock_trigger_time def get_try_num(self): """Return the number of automatic tries (try number).""" try: return self.try_timers[TimerFlags.EXECUTION_RETRY].num + 1 except (AttributeError, KeyError): return 0 def next_point(self): """Return the next cycle point.""" p_next = None adjusted = [] for seq in self.tdef.sequences: nxt = seq.get_next_point(self.point) if nxt: # may be None if beyond the sequence bounds adjusted.append(nxt) if adjusted: p_next = min(adjusted) return p_next def is_ready_to_run(self) -> Tuple[bool, ...]: """Is this task ready to run? Takes account of all dependence: on other tasks, xtriggers, and old-style ext- and clock-triggers. Or, manual triggering. """ if self.is_manual_submit: # Manually triggered, ignore unsatisfied prerequisites. return (True, ) if self.state.is_held: # A held task is not ready to run. return (False, ) if self.state.status in self.try_timers: # A try timer is still active. return (self.try_timers[self.state.status].is_delay_done(), ) return (self.state(TASK_STATUS_WAITING), self.is_waiting_clock_done(), self.is_waiting_prereqs_done()) def set_summary_time(self, event_key, time_str=None): """Set an event time in self.summary Set values of both event_key + "_time" and event_key + "_time_string". """ if time_str is None: self.summary[event_key + '_time'] = None else: self.summary[event_key + '_time'] = float(str2time(time_str)) self.summary[event_key + '_time_string'] = time_str def is_waiting_clock_done(self): """Is this task done waiting for its old-style clock trigger time? Return True if there is no clock trigger or when clock trigger is done. """ if self.tdef.clocktrigger_offset is None: return True return (time() > self.get_clock_trigger_time( str(self.tdef.clocktrigger_offset))) def is_task_prereqs_not_done(self): """Are some task prerequisites not satisfied?""" return (not all(pre.is_satisfied() for pre in self.state.prerequisites)) def is_waiting_prereqs_done(self): """Are ALL prerequisites satisfied?""" return (all(pre.is_satisfied() for pre in self.state.prerequisites) and all(tri for tri in self.state.external_triggers.values()) and self.state.xtriggers_all_satisfied()) def reset_try_timers(self): # unset any retry delay timers for timer in self.try_timers.values(): timer.timeout = None def point_match(self, point: Optional[str]) -> bool: """Return whether a string/glob matches the task's point. None is treated as '*'. """ if point is None: return True with suppress(PointParsingError): # point_str may be a glob point = standardise_point_string(point) return fnmatchcase(str(self.point), point) def status_match(self, status: Optional[str]) -> bool: """Return whether a string matches the task's status. None/an empty string is treated as a match. """ return (not status) or self.state.status == status def name_match(self, name: str) -> bool: """Return whether a string/glob matches the task's name.""" if fnmatchcase(self.tdef.name, name): return True return any( fnmatchcase(ns, name) for ns in self.tdef.namespace_hierarchy) def merge_flows(self, flow_nums: Set) -> None: """Merge another set of flow_nums with mine.""" self.flow_nums.update(flow_nums) def state_reset(self, status=None, is_held=None, is_queued=None, is_runahead=None) -> bool: """Set new state and log the change. Return whether it changed.""" before = str(self) if self.state.reset(status, is_held, is_queued, is_runahead): LOG.info(f"[{before}] => {self.state}") return True return False