Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 0.0.44

* **Ignore SIGTERM in plugin uvicorn Servers**: plugin webservers now keep
serving on SIGTERM so their controller container can finish dispatching
in-flight work before the pod is SIGKILLed. SIGINT still terminates for
local-dev Ctrl-C.

## 0.0.43

* **Deprecate `wrap_in_fastapi`** - Mark `wrap_in_fastapi` (and the `etl-uvicorn` CLI it backs) as deprecated via PEP 702 `@deprecated`. New plugins should build a FastAPI app directly with explicit handlers for the plugin contract routes.
Expand Down
35 changes: 35 additions & 0 deletions test/test_signal_handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Verify the etl_uvicorn module installs a SIGTERM-ignoring handler on uvicorn.Server."""

import asyncio
import signal

import uvicorn

# Importing the module applies the monkey-patch as a side effect.
from unstructured_platform_plugins.etl_uvicorn import main # noqa: F401


def test_uvicorn_server_install_signal_handlers_is_patched():
assert (
uvicorn.Server.install_signal_handlers.__name__
== "_install_signal_handlers_ignoring_sigterm"
)


def test_install_signal_handlers_registers_sigint_only():
async def _run() -> None:
config = uvicorn.Config(app="fake:app", lifespan="off")
server = uvicorn.Server(config=config)
server.install_signal_handlers()
loop = asyncio.get_running_loop()
try:
assert loop.remove_signal_handler(signal.SIGINT) is True
assert loop.remove_signal_handler(signal.SIGTERM) is False
finally:
try:
loop.remove_signal_handler(signal.SIGINT)
loop.remove_signal_handler(signal.SIGTERM)
except Exception:
pass

asyncio.run(_run())
2 changes: 1 addition & 1 deletion unstructured_platform_plugins/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.0.43" # pragma: no cover
__version__ = "0.0.44" # pragma: no cover
21 changes: 21 additions & 0 deletions unstructured_platform_plugins/etl_uvicorn/main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,34 @@
import asyncio
import signal
import threading
from dataclasses import dataclass, field
from typing import IO, Any, Optional

import click
import uvicorn
from uvicorn.config import LOGGING_CONFIG, Config, RawConfigParser
from uvicorn.main import main, run

from unstructured_platform_plugins.etl_uvicorn.api_generator import generate_fast_api


def _install_signal_handlers_ignoring_sigterm(self: uvicorn.Server) -> None:
# uvicorn's default load-sheds 504 on SIGTERM, which races with controllers
# trying to drain in-flight work during pod shutdown. Plugin webservers are
# expected to outlive their controller container so SIGKILL — not SIGTERM —
# ends the process. SIGINT is preserved so local Ctrl-C still works.
if threading.current_thread() is not threading.main_thread():
return
try:
loop = asyncio.get_event_loop()
loop.add_signal_handler(signal.SIGINT, self.handle_exit, signal.SIGINT, None)
except NotImplementedError:
signal.signal(signal.SIGINT, self.handle_exit)


uvicorn.Server.install_signal_handlers = _install_signal_handlers_ignoring_sigterm


@dataclass
class CustomConfig:
log_config: dict[str, Any] | str | RawConfigParser | IO[Any] | None = field(
Expand Down
Loading