@@ -40,8 +40,13 @@ class PlanError(Exception):
4040
4141
4242class PlanRunner (BaseRunner ):
43+ EXTERNAL_CALLBACK_POLL_INTERVAL_S = 1
44+ EXTERNAL_CALLBACK_WATCHDOG_TIMER_S = 60
45+
4346 def __init__ (self , context : BlueskyContext , dev_mode : bool ):
4447 super ().__init__ (context )
48+ self ._callbacks_started = False
49+ self ._callback_watchdog_expiry = time .monotonic ()
4550 self .is_dev_mode = dev_mode
4651
4752 @abstractmethod
@@ -50,27 +55,63 @@ def decode_and_execute(
5055 ) -> MsgGenerator :
5156 pass
5257
53- @abstractmethod
5458 def reset_callback_watchdog_timer (self ):
55- pass
59+ """Called periodically to reset the watchdog timer when the external callbacks ping us."""
60+ self ._callbacks_started = True
61+ self ._callback_watchdog_expiry = (
62+ time .monotonic () + self .EXTERNAL_CALLBACK_WATCHDOG_TIMER_S
63+ )
5664
5765 @property
5866 @abstractmethod
5967 def current_status (self ) -> Status :
6068 pass
6169
70+ def check_external_callbacks_are_alive (self ):
71+ callback_expiry = time .monotonic () + self .EXTERNAL_CALLBACK_WATCHDOG_TIMER_S
72+ while time .monotonic () < callback_expiry :
73+ if self ._callbacks_started :
74+ break
75+ # If on first launch the external callbacks aren't started yet, wait until they are
76+ LOGGER .info ("Waiting for external callbacks to start" )
77+ yield from bps .sleep (self .EXTERNAL_CALLBACK_POLL_INTERVAL_S )
78+ else :
79+ raise RuntimeError ("External callbacks not running - try restarting" )
80+
81+ if not self ._external_callbacks_are_alive ():
82+ raise RuntimeError (
83+ "External callback watchdog timer expired, check external callbacks are running."
84+ )
85+
86+ def request_run_engine_abort (self ):
87+ """Asynchronously request an abort from the run engine. This cannot be done from
88+ inside the main thread."""
89+
90+ def issue_abort ():
91+ try :
92+ # abort() causes the run engine to throw a RequestAbort exception
93+ # inside the plan, which will propagate through the contingency wrappers.
94+ # When the plan returns, the run engine will raise RunEngineInterrupted
95+ self .run_engine .abort ()
96+ except Exception as e :
97+ LOGGER .warning (
98+ "Exception encountered when issuing abort() to RunEngine:" ,
99+ exc_info = e ,
100+ )
101+
102+ stopping_thread = threading .Thread (target = issue_abort )
103+ stopping_thread .start ()
104+
105+ def _external_callbacks_are_alive (self ) -> bool :
106+ return time .monotonic () < self ._callback_watchdog_expiry
107+
62108
63109class InProcessRunner (PlanRunner ):
64110 """Runner that executes experiments from inside a running Bluesky plan"""
65111
66- EXTERNAL_CALLBACK_WATCHDOG_TIMER_S = 60
67- EXTERNAL_CALLBACK_POLL_INTERVAL_S = 1
68-
69112 def __init__ (self , context : BlueskyContext , dev_mode : bool ) -> None :
70113 super ().__init__ (context , dev_mode )
71114 self ._current_status : Status = Status .IDLE
72- self ._callbacks_started = False
73- self ._callback_watchdog_expiry = time .monotonic ()
74115
75116 def decode_and_execute (
76117 self , current_visit : str | None , parameter_list : Sequence [MxBlueskyParameters ]
@@ -115,20 +156,7 @@ def execute_plan(
115156 self ._current_status = Status .BUSY
116157
117158 try :
118- callback_expiry = time .monotonic () + self .EXTERNAL_CALLBACK_WATCHDOG_TIMER_S
119- while time .monotonic () < callback_expiry :
120- if self ._callbacks_started :
121- break
122- # If on first launch the external callbacks aren't started yet, wait until they are
123- LOGGER .info ("Waiting for external callbacks to start" )
124- yield from bps .sleep (self .EXTERNAL_CALLBACK_POLL_INTERVAL_S )
125- else :
126- raise RuntimeError ("External callbacks not running - try restarting" )
127-
128- if not self ._external_callbacks_are_alive ():
129- raise RuntimeError (
130- "External callback watchdog timer expired, check external callbacks are running."
131- )
159+ yield from self .check_external_callbacks_are_alive ()
132160 yield from experiment ()
133161 self ._current_status = Status .IDLE
134162 except WarningError as e :
@@ -147,35 +175,12 @@ def shutdown(self):
147175 """Performs a prompt shutdown. Aborts the run engine and terminates the loop
148176 waiting for messages."""
149177
150- def issue_abort ():
151- try :
152- # abort() causes the run engine to throw a RequestAbort exception
153- # inside the plan, which will propagate through the contingency wrappers.
154- # When the plan returns, the run engine will raise RunEngineInterrupted
155- self .run_engine .abort ()
156- except Exception as e :
157- LOGGER .warning (
158- "Exception encountered when issuing abort() to RunEngine:" ,
159- exc_info = e ,
160- )
161-
162178 LOGGER .info ("Shutting down: Stopping the run engine gracefully" )
163179 if self .current_status != Status .ABORTING :
164180 self ._current_status = Status .ABORTING
165- stopping_thread = threading .Thread (target = issue_abort )
166- stopping_thread .start ()
181+ self .request_run_engine_abort ()
167182 return
168183
169- def reset_callback_watchdog_timer (self ):
170- """Called periodically to reset the watchdog timer when the external callbacks ping us."""
171- self ._callbacks_started = True
172- self ._callback_watchdog_expiry = (
173- time .monotonic () + self .EXTERNAL_CALLBACK_WATCHDOG_TIMER_S
174- )
175-
176- def _external_callbacks_are_alive (self ) -> bool :
177- return time .monotonic () < self ._callback_watchdog_expiry
178-
179184 @property
180185 def current_status (self ) -> Status :
181186 return self ._current_status
0 commit comments