]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
manager: signals handling for shutdown (SIGINT) and config reload (SIGHUP)
authorVasek Sraier <git@vakabus.cz>
Thu, 6 Jan 2022 08:56:23 +0000 (09:56 +0100)
committerAleš Mrázek <ales.mrazek@nic.cz>
Fri, 8 Apr 2022 14:17:53 +0000 (16:17 +0200)
closes #41
closes #42

manager/knot-resolver-manager.service
manager/knot_resolver_manager/compat/asyncio.py
manager/knot_resolver_manager/kres_manager.py
manager/knot_resolver_manager/server.py

index e11de1b7192fcd8ca7c938bb50f3308f540aa895..277998c26442e2f2b0baa473190e127996663ba3 100644 (file)
@@ -6,6 +6,8 @@ After=dbus.service
 [Service]
 ExecStart=python3 -m knot_resolver_manager --config=/etc/knot-resolver/config.yml
 KillSignal=SIGINT
+# See systemd.service(5) for explanation, why we should replace this with a blocking request
+ExecReload=kill -HUP $MAINPID
 
 [Install]
 WantedBy=multi-user.target
\ No newline at end of file
index d7cf150c1c56a7f0be73e156805565b3f4cfcca3..5db8add5321a1fbcd267c8d8205b33a588e1b88c 100644 (file)
@@ -83,3 +83,8 @@ def run(coro: Coroutine[Any, T, NoneType], debug: Optional[bool] = None) -> Awai
     return loop.run_until_complete(coro)
     # asyncio.run would cancel all running tasks, but it would use internal API for that
     # so let's ignore it and let the tasks die
+
+
+def add_async_signal_handler(signal: int, callback: Callable[[], Awaitable[None]]):
+    loop = asyncio.get_event_loop()
+    loop.add_signal_handler(signal, lambda: create_task(callback()))
index 1dc772134efe5f140330dbff11919e4289d251f9..c4205afc6d19e3689e9595ddcf25c22766823e20 100644 (file)
@@ -53,6 +53,7 @@ class KresManager:
         self._watchdog_task = create_task(self._watchdog())
         await self._load_system_state()
 
+        # registering the function calls them immediately, therefore after this, the config is applied
         await config_store.register_verifier(self.validate_config)
         await config_store.register_on_change_callback(self.apply_config)
 
index 0d2fc57880e7ab0c9d3863fc7d5d2bb481c69249..4f879b884dabb2138a493173db49af3abb18f13e 100644 (file)
@@ -1,6 +1,7 @@
 import asyncio
 import logging
 import os
+import signal
 import sys
 from http import HTTPStatus
 from pathlib import Path
@@ -14,6 +15,7 @@ from aiohttp.web_response import json_response
 from aiohttp.web_runner import AppRunner, TCPSite, UnixSite
 
 from knot_resolver_manager import log
+from knot_resolver_manager.compat import asyncio as asyncio_compat
 from knot_resolver_manager.config_store import ConfigStore
 from knot_resolver_manager.constants import DEFAULT_MANAGER_CONFIG_FILE
 from knot_resolver_manager.datamodel.config_schema import KresConfig
@@ -59,7 +61,7 @@ class Server:
     # This is top-level class containing pretty much everything. Instead of global
     # variables, we use instance attributes. That's why there are so many and it's
     # ok.
-    def __init__(self, store: ConfigStore):
+    def __init__(self, store: ConfigStore, config_path: Optional[Path]):
         # config store & server dynamic reconfiguration
         self.config_store = store
 
@@ -69,6 +71,7 @@ class Server:
         self.listen: Optional[Listen] = None
         self.site: Union[NoneType, TCPSite, UnixSite] = None
         self.listen_lock = asyncio.Lock()
+        self._config_path: Optional[Path] = config_path
 
         self.shutdown_event = asyncio.Event()
 
@@ -86,8 +89,27 @@ class Server:
 
         return Result.ok(None)
 
+    async def sigint_handler(self):
+        logger.info("Received SIGINT, triggering graceful shutdown")
+        self.shutdown_event.set()
+
+    async def sighup_handler(self) -> None:
+        logger.info("Received SIGHUP, reloading configuration file")
+        if self._config_path is None:
+            logger.warning("The manager was started with inlined configuration - can't reload")
+        else:
+            data = await readfile(self._config_path)
+            config = KresConfig(parse_yaml(data))
+
+            try:
+                await self.config_store.update(config)
+            except KresdManagerException as e:
+                logger.error(f"Reloading of the configuration file failed. {e}")
+
     async def start(self):
         self._setup_routes()
+        asyncio_compat.add_async_signal_handler(signal.SIGINT, self.sigint_handler)
+        asyncio_compat.add_async_signal_handler(signal.SIGHUP, self.sighup_handler)
         await self.runner.setup()
         await self.config_store.register_verifier(self._deny_listen_address_changes)
         await self.config_store.register_on_change_callback(self._reconfigure)
@@ -225,9 +247,6 @@ _DEFAULT_SENTINEL = _DefaultSentinel()
 
 async def _load_raw_config(config: Union[Path, ParsedTree, _DefaultSentinel]) -> ParsedTree:
     # Initial configuration of the manager
-    if isinstance(config, _DefaultSentinel):
-        # use default
-        config = DEFAULT_MANAGER_CONFIG_FILE
     if isinstance(config, Path):
         if not config.exists():
             raise KresdManagerException(
@@ -286,12 +305,20 @@ def _set_working_directory(config_raw: ParsedTree):
     os.chdir(config.server.rundir.to_path())
 
 
-async def start_server(config: Union[Path, ParsedTree, _DefaultSentinel] = _DEFAULT_SENTINEL):
+async def start_server(config: Union[Path, ParsedTree] = DEFAULT_MANAGER_CONFIG_FILE):
     start_time = time()
+    manager: Optional[KresManager] = None
+
+    # Block signals during initialization to force their processing once everything is ready
+    signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGINT, signal.SIGHUP})
 
     # before starting server, initialize the subprocess controller, config store, etc. Any errors during inicialization
     # are fatal
     try:
+        # Make sure that the config path does not change meaning when we change working directory
+        if isinstance(config, Path):
+            config = config.absolute()
+
         # Preprocess config - load from file or in general take it to the last step before validation.
         config_raw = await _load_raw_config(config)
 
@@ -326,10 +353,13 @@ async def start_server(config: Union[Path, ParsedTree, _DefaultSentinel] = _DEFA
 
     # At this point, all backend functionality-providing components are initialized. It's therefore save to start
     # the API server.
-    server = Server(config_store)
+    server = Server(config_store, config if isinstance(config, Path) else None)
     await server.start()
     logger.info(f"Manager fully initialized and running in {round(time() - start_time, 3)} seconds")
 
+    # Now we are ready to process all signals
+    signal.pthread_sigmask(signal.SIG_UNBLOCK, {signal.SIGINT, signal.SIGHUP})
+
     await server.wait_for_shutdown()
 
     # After triggering shutdown, we neet to clean everything up