Source code for laborchestrator.engine.worker_observer
"""
This class observes running process steps. When delays or errors occur, jobs get finished the WFGManager,
WorkerInterface or ScheduleManager are notified accordingly.
"""
from laborchestrator.structures import SchedulingInstance, StepStatus, ProcessStep
from laborchestrator.engine import WFGManager, ScheduleManager, WorkerInterface
import time
from threading import Thread
from datetime import datetime
import traceback
from laborchestrator.logging_manager import StandardLogger as Logger
from typing import Tuple, Set
[docs]
class WorkerObserver:
worker: WorkerInterface
wfg_manager: WFGManager
schedule_manager: ScheduleManager
jssp: SchedulingInstance
observed_jobs: Set[str]
def __init__(self, wfg_manager: WFGManager, schedule_manager: ScheduleManager, jssp: SchedulingInstance,
worker: WorkerInterface):
self.jssp = jssp
self.wfg_manager = wfg_manager
self.schedule_manager = schedule_manager
self.worker = worker
self.observe_thread = Thread(daemon=True, target=self.observe)
self.observe_thread.start()
self.observed_jobs = set()
[docs]
def observe(self):
"""
Master thread
:return:
"""
while True:
try:
time.sleep(.2)
steps_to_observe = list(self.worker.observation_handlers.keys())
for step_id in steps_to_observe:
# check for new started jobs
if step_id not in self.observed_jobs:
# start a thread to observe this job
Thread(daemon=True, target=self._observe_protocol, args=[step_id]).start()
except Exception as ex:
Logger.warning(f"Worker got an exception: {ex}, {traceback.format_exc()}")
[docs]
def _observe_protocol(self, step_id: str):
Logger.debug(f"start observing execution of {step_id}")
self.observed_jobs.add(step_id)
protocol_info = self.worker.observation_handlers[step_id]
job = self.jssp.step_by_id[step_id]
error_count = 0
while True:
try:
if step_id not in self.observed_jobs or step_id not in self.jssp.step_by_id:
# this happens after error recovery. In this case, we interrupt without doing anything
Logger.info(f"There seems to have happened a error recovery or process removal of step {step_id}. Stopping to observer it.")
return
# this happens if the job's status gets manipulated from somewhere else (e.g. error-recovery)
if job.status == StepStatus.FINISHED:
self.worker.process_step_finished(step_id, job.result)
break
# get the current status from the Observable protocol_info
status = StepStatus(protocol_info.status.value)
if status == StepStatus.RUNNING:
self._handle_running(job)
elif status == StepStatus.ERROR:
self._handle_error(job, protocol_info)
break
elif status == StepStatus.FINISHED:
self._handle_finished(step_id, job, protocol_info)
break
time.sleep(.5)
error_count = 0
except Exception as ex:
Logger.error(f"{ex}\n{traceback.format_exc()}")
error_count += 1
if error_count > 10:
Logger.warning(f"Too many errors while observing step {step_id}. Stopping observation.")
break
[docs]
def _handle_running(self, job: ProcessStep) -> None:
job.status = StepStatus.RUNNING
# reschedule if the job takes longer than expected
delayed, duration_increase = self.is_delayed_significantly(job)
if delayed:
Logger.warning(f"job {job.name} is delayed by {(datetime.today() - job.start).total_seconds() - job.duration}")
job.duration += duration_increase
# jobs taking longer than expected can lead to serious problems if schedule is not adapted
self.schedule_manager.mark_schedule_invalid()
[docs]
def _handle_error(self, job: ProcessStep, protocol_info) -> None:
# retrieve the error message for the GUI and record the error state via the shared helper
message = self._get_responses_safe(protocol_info, log_prefix="error retrieving response")
self.worker.set_step_error(job, message)
[docs]
def _handle_finished(self, step_id: str, job: ProcessStep, protocol_info) -> None:
job.finish = datetime.today()
predicted_duration = job.duration
job.duration = (job.finish - job.start).total_seconds()
# reschedule if we saved significantly much time
if job.duration + 2 * self.schedule_manager.time_limit_short < predicted_duration:
self.schedule_manager.mark_schedule_suboptimal()
# handle job effect on container_info
job.result = self._get_responses_safe(protocol_info, log_exception=True)
self.worker.process_step_finished(step_id, job.result)
job.status = StepStatus.FINISHED # do this AFTER the effect of the step was calculated,
# or other steps might get started with insufficient information
[docs]
def _get_responses_safe(self, protocol_info, log_prefix: str = "", log_exception: bool = False) -> str:
try:
return protocol_info.get_responses()
except Exception as ex:
res = str(ex)
if log_prefix:
Logger.error(f"{log_prefix}: {res}")
if log_exception:
Logger.error(f"{ex}\n{traceback.format_exc()}")
return res + str(traceback.format_exc())
return res
[docs]
def is_delayed_significantly(self, job: ProcessStep) -> Tuple[bool, float]:
delay = (datetime.today() - job.start).total_seconds() - job.duration
offset = self.schedule_manager.time_limit_short
delayed = delay > 1
duration_increase = 2 * offset
return delayed, duration_increase