From ad2cbbd871554f67184c290a5a23440d9fe7283a Mon Sep 17 00:00:00 2001 From: Benjamin Labrecque Date: Thu, 20 Aug 2026 09:37:22 +0200 Subject: [PATCH] refactor: extract base runner class --- packages/agebd/src/agebd/runner.py | 162 +++++++++++++++-------------- 1 file changed, 82 insertions(+), 80 deletions(-) diff --git a/packages/agebd/src/agebd/runner.py b/packages/agebd/src/agebd/runner.py index 6a3544c..5429004 100644 --- a/packages/agebd/src/agebd/runner.py +++ b/packages/agebd/src/agebd/runner.py @@ -9,107 +9,109 @@ from agebd.service.base import BaseService logger = logging.getLogger(__name__) -class CallbackRunner: +class _BaseRunner: + """ + Shared control loop: poll abort/onoff PVs, dispatch to the active or + paused tick, ping the alive watchdog, and stop cleanly on exception. + """ + + def __init__(self, service: BaseService): + self.svc = service + self.pvs = service.pvs + + def start(self): + logger.debug(f"Starting {type(self).__name__} for service: {self.svc.name}") + while not self.pvs.abort.get(): + try: + # if service is not paused + if self.pvs.onoff.get(): + self._tick_active() + # if service is paused + else: + self._tick_paused() + + # inform watchdog of service alive state + self.pvs.alive.put(0) + + except Exception as e: + self.svc.exception = e + self.svc.crashed = True + + break + + self._on_stop() + self.svc.finalize_termination() + + def _tick_active(self): + raise NotImplementedError + + def _tick_paused(self): + raise NotImplementedError + + def _on_stop(self): + pass + + +class CallbackRunner(_BaseRunner): """ Executes service updates asynchronously, triggered by EPICS PV events. """ - def __init__(self, service: BaseService): - self.svc = service - self.pvs = service.pvs - self._callback_active = False + def _tick_active(self): + # add callback for triggering the loop if not active + if not self.svc.CallbackActive: + self.svc.transition_to_running() + self.pvs.CallbackPV.add_callback(callback=self.svc.CallbackFun, index=1) + self.svc.CallbackActive = 1 - def start(self): - logger.debug(f"Starting CallbackRunner for service: {self.svc.name}") - while not self.pvs.abort.get(): - try: - # if service is not paused - if self.pvs.onoff.get(): - # add callback for triggering the loop if not active - if not self._callback_active: - self.svc.transition_to_running() - self.pvs.CallbackPV.add_callback(callback=self.svc.CallbackFun, index=1) - self.svc.CallbackActive = 1 + # run mainloop of service + self.svc.update() - # run mainloop of service - self.svc.update() + # inform watchdog of service run state + self.pvs.running.put(0) - # inform watchdog of service run state - self.pvs.running.put(0) + # wait for callback to fire + poll(evt=1.0e-5, iot=0.1) - # wait for callback to fire - poll(evt=1.0e-5, iot=0.1) + def _tick_paused(self): + # remove callback for triggering the loop + if self.svc.CallbackActive: + self.svc.transition_to_paused() + self.pvs.CallbackPV.remove_callback(index=1) + self.svc.CallbackActive = 0 + self.svc.CallbackFired = 0 - # if service is paused - else: - # remove callback for triggering the loop - if self.svc.CallbackActive: - self.svc.transition_to_paused() - self.pvs.CallbackPV.remove_callback(index=1) - self.svc.CallbackActive = 0 - self.svc.CallbackFired = 0 - - # wait until service is unpaused - sleep(0.1) - - # inform watchdog of service alive state - self.pvs.alive.put(0) - - except Exception as e: - self.svc.exception = e - self.svc.crashed = True - - break + # wait until service is unpaused + sleep(0.1) + def _on_stop(self): # remove callback(s) from event(s) self.pvs.CallbackPV.remove_callback(index=1) - self.svc.finalize_termination() -class PeriodicRunner: +class PeriodicRunner(_BaseRunner): """ Executes service updates at a fixed frequency. """ - def __init__(self, service: BaseService): - self.svc = service - self.pvs = service.pvs + def _tick_active(self): + if not self.svc.status == ServiceStatus.RUNNING: + self.svc.transition_to_running() - def start(self): - logger.debug(f"Starting PeriodicRunner for service: {self.svc.name}") - while not self.pvs.abort.get(): - try: - # if service is not paused - if self.pvs.onoff.get(): - if not self.svc.status == ServiceStatus.RUNNING: - self.svc.transition_to_running() + # run mainloop of service + self.svc.update() - # run mainloop of service - self.svc.update() + # inform watchdog of service run state + self.pvs.running.put(0) - # inform watchdog of service run state - self.pvs.running.put(0) + # wait for timer + sleep(self.svc.sleep_interval) - # wait for timer - sleep(self.svc.sleep_interval) + def _tick_paused(self): + if not self.svc.status == ServiceStatus.PAUSED: + self.svc.transition_to_paused() - # if service is paused - else: - if not self.svc.status == ServiceStatus.PAUSED: - self.svc.transition_to_paused() + self.svc.CallbackFired = 0 - self.svc.CallbackFired = 0 - - # wait until service is unpaused - sleep(0.1) - - # inform watchdog of service alive state - self.pvs.alive.put(0) - - except Exception as e: - self.svc.exception = e - self.svc.crashed = True - - break - - self.svc.finalize_termination() + # wait until service is unpaused + sleep(0.1)