panel
panel copied to clipboard
panel.serve() implicitly and unconditionally captures SIGINT under the hood
ALL software version info
panel 1.4.4 (currently latest). Others are irrelevant, I believe, but still:
- python 3.9
- bokeh 3.4.1
- OS Windows 11
- browser FireFox (definitely irrelevant)
Description of expected behavior and the observed behavior
Observed:
panel.serve(..., threaded=False)
delegates to panel.io.server.get_server()
.
This in turn attempts (with silent failure!?) to install a SIGINT
handler that eventually calls server.io_loop.stop()
.
This behavior stops the asyncio loop in its tracks without allowing any opportunity for code that shares the same event loop to perform its own orderly cleanup.
Note also that the signal handler is installed unconditionally; no arguments can be passed in to prevent it. In particular, this is done even when start=False
is passed.
Expected:
IMHO, being intended for programmatic server operation from user code, it is none of panel.serve()
's business to do ANY kind of OS signal handling. Also, it is not within its charter to stop the event loop it is running on.
All of this should be handled by higher level code that has better awareness of the environment it is being run in, and what else may be running in it.
I expect panel.serve()
, especially if called with start=False
, to just create a server. That's it. Not start()
and definitely not stop()
it for me.
Even if this overreach is somehow deemed to be within the charter of panel.serve()
in some cases, there should be a mechanism in place to allow the caller to prevent this when undesired.
Complete, minimal, self-contained example code that reproduces the issue
In the following program, if SIGINT is received while in the try
block, things will explode.
This is because the event loop gets stopped by panel.serve
while the coroutine is inside asyncio.wait(tasks_running, ...)
, and the finally
clause never gets to run.
fueling_dashboard: panel.viewable.Viewable
class SpaceShip:
async def monitor_fuel_tanks(self, how: str):
while True:
fueling_dashboard.update_fuel_display(self.poll_fuel_sensors(how))
await asyncio.sleep(0.5)
async def make_launch_preparations(self, ...):
from asyncio import create_task, ALL_COMPLETED
fueling_dashboard_server = panel.serve(fueling_dashboard, start=False, threaded=False, ...)
panel.state.execute(self.monitor_fuel_tanks('carefully')) # schedule background update task before starting server
fueling_dashboard_server.start()
tasks_running = {create_task(self.pump_in_oxygen(fill_level=0.95)), create_task(self.pump_in_hydrogen(fill_level=0.95))}
try:
_, tasks_running = await asyncio.wait(tasks_running, return_when=ALL_COMPLETED)
finally:
was_successful = not tasks_running
if not was_successful: # something went wrong; e.g. KeyboardInterrupt or asyncio.CancelledError because SIGINT was received
fueling_dashboard.big_red_flashing_lamp.turn_on()
for task in tasks_running: # stop pumping in
task.cancel()
# empty tanks to prevent explosion
await asyncio.wait({self.pump_out_oxygen(), self.pump_out_hydrogen()}, return_when=ALL_COMPLETED)
fueling_dashboard_server.stop() # no longer needed
return was_successful
Stack traceback and/or browser JavaScript console output
not applicable
Screenshots or screencasts of the bug in action
not applicable
- [x] I may be interested in making a pull request to address this (but I'm not sure I know enough about what else could break)