Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

ASGI app SIGTERM handler silently replaces the hosting server's handler

Abierto
#236 0 comentarios 0 reacciones 0 asignados Ver en GitHub

@sharosoo ya está trabajando en esto.

Desde el 18/10/2026.

  • #240 de @sharosoo — abierto

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
52/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
python
Área
backend, devops

Línea de trabajo

Read python/restate/server.py:254-270 and inspect how asgi_app registers the SIGTERM handler for request and lifespan scopes. Reproduce the displacement with the provided minimal repro, then check behavior with both loop.add_signal_handler and signal.signal. Done means the host server's shutdown handler still runs after restate.app(...) is called, while active receive channels are notified.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

On the first call into restate.app(...) of any scope, asgi_app registers loop.add_signal_handler(signal.SIGTERM, _on_sigterm) (python/restate/server.py:254-267). Because loop.add_signal_handler replaces the existing handler without chaining, SIGTERM no longer reaches the hosting ASGI server's shutdown path after the first request.

Observed behavior

Measured on restate-sdk 1.0.5 (main 63d2fe5), Python 3.13:

  • Hypercorn 0.18: serving a plain ASGI app exits in ~0.1 s on SIGTERM. Serving restate.app(...), hitting /health once, and sending SIGTERM leaves the process running past a 15 s grace period until SIGKILL.
  • Uvicorn 0.54.0: serving a plain ASGI app exits in ~0.1 s on SIGTERM. Serving restate.app(...) after one request hangs the same way until SIGKILL.
  • Lifespan scope: a single lifespan.startup call is enough to displace the host handler, because the signal handler is installed before the scope["type"] == "lifespan" check (python/restate/server.py:269-270).

_on_sigterm only calls notify_shutdown() on active ReceiveChannel instances. Nothing notifies the host server to stop accepting connections or exit.

Why both loop.add_signal_handler and signal.signal matter

ASGI servers register SIGTERM in two ways on Unix:

  1. Uvicorn uses signal.signal(signal.SIGTERM, ...).
  2. Hypercorn uses loop.add_signal_handler(signal.SIGTERM, ...) (hypercorn/asyncio/run.py). Once loop.add_signal_handler is active, Python's signal.getsignal(signal.SIGTERM) returns asyncio.unix_events._sighandler_noop, and the actual host callback lives in loop._signal_handlers[signal.SIGTERM] (an asyncio.Handle storing the callback and its arguments).

PR #226 adds re-dispatch using signal.getsignal(signal.SIGTERM) before loop.add_signal_handler. Tested on top of 1.0.5, #226 fixes Uvicorn, but Hypercorn still hangs for 15 s because signal.getsignal(signal.SIGTERM) only captures _sighandler_noop rather than the previous loop handler.

Suggested fix

Before calling loop.add_signal_handler(signal.SIGTERM, _on_sigterm):

  1. Check getattr(loop, "_signal_handlers", {}).get(signal.SIGTERM) first. If present, save that asyncio.Handle and invoke prev_handle._run() from _on_sigterm after notifying the channels.
  2. Otherwise, fall back to signal.getsignal(signal.SIGTERM) and invoke it as prev(signal.SIGTERM, None) when callable and not asyncio.unix_events._sighandler_noop.
  3. Optionally move the registration after the scope["type"] == "lifespan" check so a rejected lifespan call does not touch process signals.
Minimal repro (no external server required)
# uv run --no-project --python 3.13 --with restate-sdk==1.0.5 python repro.py
import asyncio
import signal
import restate

calls: list[str] = []


def build_app():
    svc = restate.Service("probe")

    @svc.handler()
    async def noop(ctx: restate.Context) -> str:
        return "ok"

    return restate.app(services=[svc])


async def one_call(app):
    async def receive():
        return {"type": "lifespan.startup"}

    async def send(msg):
        pass

    try:
        await app({"type": "lifespan", "asgi": {"version": "3.0"}}, receive, send)
    except Exception:
        pass


def run_case(style: str) -> bool:
    calls.clear()
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    if style == "loop.add_signal_handler":
        loop.add_signal_handler(signal.SIGTERM, lambda: calls.append("host_ran"))
    elif style == "signal.signal":
        signal.signal(signal.SIGTERM, lambda *_: calls.append("host_ran"))

    app = build_app()
    loop.run_until_complete(one_call(app))
    signal.raise_signal(signal.SIGTERM)
    loop.run_until_complete(asyncio.sleep(0.05))
    print(f"{style:<24} -> {calls or ['displaced (nothing ran)']}")
    return "host_ran" in calls


for mode in ("loop.add_signal_handler", "signal.signal"):
    run_case(mode)

Output on restate-sdk 1.0.5:

loop.add_signal_handler  -> ['displaced (nothing ran)']
signal.signal            -> ['displaced (nothing ran)']
Lenguaje dominante
Python
Estrellas
83
Forks
26
Merge medio
7 d 4 h
PR fusionados (30 d)
3

Preparar el entorno

Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de restatedev/sdk-python

Todos los issues de restatedev/sdk-python

Issues similares

Más issues de Python

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.