Skip to content

Commit 7b71049

Browse files
authored
WIP: Adding some signal handling (#27)
Adding some signal handling Signed-off-by: Matthias Wessendorf <[email protected]>
1 parent d32f5c2 commit 7b71049

File tree

2 files changed

+51
-7
lines changed

2 files changed

+51
-7
lines changed

src/func_python/http.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import asyncio
33
import logging
44
import os
5+
import signal
56
import hypercorn.config
67
import hypercorn.asyncio
78

@@ -10,7 +11,6 @@
1011

1112
logging.basicConfig(level=DEFAULT_LOG_LEVEL)
1213

13-
1414
def serve(f):
1515
"""serve a function f by wrapping it in an ASGI web application
1616
and starting. The function can be either a constructor for a functon
@@ -46,6 +46,7 @@ async def __call__(self, scope, receive, send):
4646
class ASGIApplication():
4747
def __init__(self, f):
4848
self.f = f
49+
self.stop_event = asyncio.Event()
4950
# Inform the user via logs that defaults will be used for health
5051
# endpoints if no matchin methods were provided.
5152
if hasattr(self.f, "alive") is not True:
@@ -67,7 +68,18 @@ def serve(self):
6768
cfg.bind = [os.getenv('LISTEN_ADDRESS', DEFAULT_LISTEN_ADDRESS)]
6869

6970
logging.debug(f"function starting on {cfg.bind}")
70-
return asyncio.run(hypercorn.asyncio.serve(self, cfg))
71+
return asyncio.run(self._serve(cfg))
72+
73+
async def _serve(self, cfg):
74+
loop = asyncio.get_event_loop()
75+
loop.add_signal_handler(signal.SIGINT, self._handle_signal)
76+
loop.add_signal_handler(signal.SIGTERM, self._handle_signal)
77+
78+
await hypercorn.asyncio.serve(self, cfg)
79+
80+
def _handle_signal(self):
81+
logging.info("Signal received: initiating shutdown")
82+
self.stop_event.set()
7183

7284
async def on_start(self):
7385
"""on_start handles the ASGI server start event, delegating control
@@ -82,8 +94,8 @@ async def on_stop(self):
8294
self.f.stop()
8395
else:
8496
logging.info("function does not implement 'stop'. Skipping.")
97+
self.stop_event.set()
8598

86-
# Register ASGIFunctoin as a callable ASGI Function
8799
async def __call__(self, scope, receive, send):
88100
if scope['type'] == 'lifespan':
89101
while True:
@@ -101,7 +113,7 @@ async def __call__(self, scope, receive, send):
101113
# Assert request is HTTP
102114
if scope["type"] != "http":
103115
await send_exception(send, 400,
104-
"Functions currenly only support ASGI/HTTP "
116+
"Functions currently only support ASGI/HTTP "
105117
f"connections. Got {scope['type']}"
106118
)
107119
return

tests/test_http.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
LISTEN_ADDRESS = os.getenv("LISTEN_ADDRESS")
1515

1616
def test_static():
17-
"""
17+
"""
1818
ensures that a user function developed using the default "static"
1919
style (method signature) is served by the middleware.
2020
"""
@@ -71,9 +71,8 @@ def test():
7171

7272
test_thread.join(timeout=5)
7373

74-
7574
def test_instanced():
76-
"""
75+
"""
7776
ensures that a user function developed using the default "instanced"
7877
style is served by the middleware
7978
"""
@@ -132,3 +131,36 @@ def test():
132131
logging.info("signal received")
133132

134133
test_thread.join(timeout=5)
134+
135+
def test_signal_handling():
136+
"""
137+
Tests that the server gracefully shuts down when receiving a SIGINT signal.
138+
"""
139+
# Example minimal ASGI app
140+
async def handle(scope, receive, send):
141+
await send({
142+
'type': 'http.response.start',
143+
'status': 200,
144+
'headers': [[b'content-type', b'text/plain']],
145+
})
146+
await send({
147+
'type': 'http.response.body',
148+
'body': b'Signal Handling OK',
149+
})
150+
151+
# Function to send a SIGINT after a delay
152+
def send_signal():
153+
time.sleep(2) # Allow server to start
154+
os.kill(os.getpid(), signal.SIGINT)
155+
156+
# Start signal sender in a separate thread
157+
signal_thread = threading.Thread(target=send_signal)
158+
signal_thread.start()
159+
160+
# Serve the function
161+
try:
162+
serve(handle)
163+
except KeyboardInterrupt:
164+
logging.info("SIGINT received and handled gracefully.")
165+
166+
signal_thread.join(timeout=5)

0 commit comments

Comments
 (0)