Monkey patch Uvicorn's handle_exit function to stop streams
Before the uvicorn handle_exit function is called kill the camera streams.
This commit is contained in:
parent
421efa2866
commit
d9fd54696a
2 changed files with 58 additions and 1 deletions
|
|
@ -1,15 +1,42 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional, Callable
|
||||||
from copy import copy
|
from copy import copy
|
||||||
|
|
||||||
import labthings_fastapi as lt
|
import labthings_fastapi as lt
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
import logging
|
||||||
|
from uvicorn.main import Server
|
||||||
from .serve_static_files import add_static_files
|
from .serve_static_files import add_static_files
|
||||||
from .legacy_api import add_v2_endpoints
|
from .legacy_api import add_v2_endpoints
|
||||||
from ..logging import configure_logging, retrieve_log, retrieve_log_from_file
|
from ..logging import configure_logging, retrieve_log, retrieve_log_from_file
|
||||||
|
|
||||||
|
|
||||||
|
def set_shutdown_function(shutdown_function: Callable[[], None]):
|
||||||
|
"""
|
||||||
|
Ensure a function is called before the shutdown
|
||||||
|
|
||||||
|
This monkey patches the Uvicorn Server's handle_exit. This is needed because
|
||||||
|
the uvicorn `lifecycle` events and FastAPI `shutdown` events only fire once
|
||||||
|
background tasks have completed.
|
||||||
|
|
||||||
|
Without this the system exits cleanly only if no client is receiving a
|
||||||
|
StreamingResponse. This patch is used to stop the async generators that
|
||||||
|
send streaming responses.
|
||||||
|
|
||||||
|
:param shutdown_function: A callable with no arguments or outputs. This
|
||||||
|
should stop any async generators that may be sending to streaming responses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
original_handler = Server.handle_exit
|
||||||
|
|
||||||
|
def handle_exit(*args, **kwargs):
|
||||||
|
shutdown_function()
|
||||||
|
original_handler(*args, **kwargs)
|
||||||
|
|
||||||
|
Server.handle_exit = handle_exit
|
||||||
|
|
||||||
|
|
||||||
def customise_server(
|
def customise_server(
|
||||||
server: lt.ThingServer, log_folder: str, scans_folder: Optional[str]
|
server: lt.ThingServer, log_folder: str, scans_folder: Optional[str]
|
||||||
):
|
):
|
||||||
|
|
@ -57,6 +84,21 @@ def serve_from_cli(argv: Optional[list[str]] = None):
|
||||||
scans_folder = _get_scans_dir(config)
|
scans_folder = _get_scans_dir(config)
|
||||||
server = lt.cli.server_from_config(config)
|
server = lt.cli.server_from_config(config)
|
||||||
customise_server(server, log_folder, scans_folder)
|
customise_server(server, log_folder, scans_folder)
|
||||||
|
|
||||||
|
def shutdown_call():
|
||||||
|
try:
|
||||||
|
# Kill any mjpeg streams so that StreamingResponses close.
|
||||||
|
server.things["/camera/"].kill_mjpeg_streams()
|
||||||
|
except BaseException as e:
|
||||||
|
# Catch anything and log as it is essential that this
|
||||||
|
# function cannot raise an unhandled exception or Uvicorn
|
||||||
|
# will never get a shutdown signal.
|
||||||
|
logging.error(e, exc_info=True)
|
||||||
|
|
||||||
|
# Monkey patch uvicorn's exit handling to stop the MJPEG Streams
|
||||||
|
# before waiting for background tasks to complete.
|
||||||
|
set_shutdown_function(shutdown_call)
|
||||||
|
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
server.app,
|
server.app,
|
||||||
host=args.host,
|
host=args.host,
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,21 @@ class BaseCamera(lt.Thing):
|
||||||
"CameraThings must define their own start_streaming method"
|
"CameraThings must define their own start_streaming method"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def kill_mjpeg_streams(self):
|
||||||
|
"""
|
||||||
|
Kill the streams now as the server is shutting down.
|
||||||
|
|
||||||
|
This is called when uvicorn gets the a shutdown signal. As this is called from
|
||||||
|
the event loop it cannot interact with the our ThingProperties or run
|
||||||
|
`self.mjpeg_stream.stop()` as the portal cannot be called from this loop.
|
||||||
|
|
||||||
|
Instead we just set the `_streaming` value to False. This stops the async frame
|
||||||
|
generator when the next frame notifies.
|
||||||
|
"""
|
||||||
|
if self.stream_active:
|
||||||
|
self.mjpeg_stream._streaming = False
|
||||||
|
self.lores_mjpeg_stream._streaming = False
|
||||||
|
|
||||||
@lt.thing_property
|
@lt.thing_property
|
||||||
def stream_active(self) -> bool:
|
def stream_active(self) -> bool:
|
||||||
"Whether the MJPEG stream is active"
|
"Whether the MJPEG stream is active"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue