Skip to content

Commit 2f9a530

Browse files
committed
add signal service, building block for a full gracefull shutdown implementation
1 parent b229648 commit 2f9a530

File tree

1 file changed

+71
-0
lines changed

1 file changed

+71
-0
lines changed

async_utils/sig_service.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Copyright 2020-present Michael Hall
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
import select
18+
import signal
19+
import socket
20+
import sys
21+
from collections.abc import Callable
22+
from types import FrameType
23+
from typing import Any
24+
25+
type SignalCallback = Callable[[signal.Signals], Any]
26+
type StartStopCall = Callable[[], Any]
27+
type _HANDLER = Callable[[int, FrameType | None], Any] | int | signal.Handlers | None
28+
29+
__all__ = ["SignalService"]
30+
31+
possible = "SIGINT", "SIGTERM", "SIGBREAK", "SIGHUP"
32+
actual = tuple(e for name, e in signal.Signals.__members__.items() if name in possible)
33+
34+
35+
class SignalService:
36+
"""Meant for graceful signal handling where the main thread is only used for signal handling.
37+
This should be paired with event loops being run in threads."""
38+
39+
def __init__(self, *, startup: list[StartStopCall], signal_cbs: list[SignalCallback], joins: list[StartStopCall]) -> None:
40+
self._startup: list[StartStopCall] = startup
41+
self._cbs: list[SignalCallback] = signal_cbs
42+
self._joins: list[StartStopCall] = joins
43+
44+
def run(self):
45+
ss, cs = socket.socketpair()
46+
ss.setblocking(False)
47+
cs.setblocking(False)
48+
signal.set_wakeup_fd(cs.fileno())
49+
50+
original_handlers: list[_HANDLER] = []
51+
52+
for sig in actual:
53+
original_handlers.append(signal.getsignal(sig))
54+
signal.signal(sig, lambda s, f: None)
55+
if sys.platform != "win32":
56+
signal.siginterrupt(sig, False)
57+
58+
for task_start in self._startup:
59+
task_start()
60+
61+
select.select([ss], [], [])
62+
data, *_ = ss.recv(4096)
63+
64+
for cb in self._cbs:
65+
cb(signal.Signals(data))
66+
67+
for join in self._joins:
68+
join()
69+
70+
for sig, original in zip(actual, original_handlers):
71+
signal.signal(sig, original)

0 commit comments

Comments
 (0)