ASGI app SIGTERM handler silently replaces the hosting server's handler
Avaliação
- Dificuldade
- 4/5
- Tempo estimado
- 3-5 dias
- Facilidade para iniciantes
- 52/100
Direção de pesquisa
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.
Escrita pelo modelo de indexação a partir do texto da issue.
Descrição
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/healthonce, 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.startupcall is enough to displace the host handler, because the signal handler is installed before thescope["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:
- Uvicorn uses
signal.signal(signal.SIGTERM, ...). - Hypercorn uses
loop.add_signal_handler(signal.SIGTERM, ...)(hypercorn/asyncio/run.py). Onceloop.add_signal_handleris active, Python'ssignal.getsignal(signal.SIGTERM)returnsasyncio.unix_events._sighandler_noop, and the actual host callback lives inloop._signal_handlers[signal.SIGTERM](anasyncio.Handlestoring 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):
- Check
getattr(loop, "_signal_handlers", {}).get(signal.SIGTERM)first. If present, save thatasyncio.Handleand invokeprev_handle._run()from_on_sigtermafter notifying the channels. - Otherwise, fall back to
signal.getsignal(signal.SIGTERM)and invoke it asprev(signal.SIGTERM, None)when callable and notasyncio.unix_events._sighandler_noop. - 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)']
- Linguagem predominante
- Python
- Estrelas
- 83
- Forks
- 26
- Merge médio
- 7d 4h
- PRs com merge (30d)
- 3
Preparar o ambiente
Este projeto não oferece contêiner de desenvolvimento, Dockerfile nem guia de contribuição, então a configuração fica por sua conta: comece pelo README e veja nosso guia da primeira contribuição para os passos gerais.
Primeiros passos
- Leia a issue inteira e depois o guia de contribuição do projeto.
- Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
- Faça um fork do repositório e trabalhe em uma branch.
- Abra um pull request que referencie o número da issue.
Mais de restatedev/sdk-python
-
pydantic ext: request_stream() never initializes the turnstile — KeyError('call_...') on first tool call of a streamed runTalvez já em andamento @QuocThuanTruong assumiu há 90 dias. Aberta
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 76/100
restatedev/sdk-python#223 · 1 comentário ·
-
Dificuldade 2/5 1-2 dias Facilidade para iniciantes 68/100
restatedev/sdk-python#204 · 1 comentário ·
-
pydantic ext: tool results lose ToolReturn and deferred metadata, turnstile breaks on deferred and nested calls, iter() is not durableTalvez já em andamento @sharosoo assumiu há 7 dias. Aberta
Dificuldade 4/5 3-5 dias Facilidade para iniciantes 48/100
restatedev/sdk-python#237 ·
-
RestateModelWrapper should not wrap `SdkInternalBaseException` as generic Exception; classify non-retryable provider errors as terminalTalvez já em andamento Um pull request vinculado a esta issue está aberto ou já foi mesclado. Aberta
Dificuldade 4/5 3-5 dias Facilidade para iniciantes 48/100
restatedev/sdk-python#225 ·
-
pydantic ext: auto_wrap_tools + async event_stream_handler crashes with 'Object of type coroutine is not JSON serializable' on first tool callTalvez já em andamento @QuocThuanTruong assumiu há 90 dias. Aberta
Dificuldade 3/5 1-2 dias Facilidade para iniciantes 68/100
restatedev/sdk-python#222 · 1 comentário ·
Todas as issues de restatedev/sdk-python
Issues semelhantes
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 68/100
Arekkazu/sgpmp-backend#744 ·
Mantenedores costumam responder em até 1 dia
-
Review backlog (security): master-branch remote guard trusts unbound provenance package fieldAbertaagent/reviewer
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
projectbluefin/utah-packages#634 ·
Mantenedores costumam responder em até 1 dia
-
enhancement good first issue help wanted
Dificuldade 2/5 1-2 dias Facilidade para iniciantes 78/100
Mantenedores costumam responder em até 1 dia
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 68/100
home-assistant/core#188613 ·
Mantenedores costumam responder em até 1 dia
-
WebsiteAbertagood first issue
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 91/100
TanbirRamim/open-source-radar#314 ·
Mantenedores costumam responder em até 1 dia