Coverage for src/lilbee/app/config_follow.py: 100%
38 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
1"""Take what another process wrote to config.toml before a long-running process answers."""
3from __future__ import annotations
5import asyncio
6import logging
7import threading
9from lilbee.app.settings import (
10 ConfigReading,
11 SettingsUpdateResult,
12 adopt_external_changes,
13 start_config_reading,
14)
15from lilbee.core import settings as persistent_settings
16from lilbee.core.config import cfg
17from lilbee.core.config.keys import PROVIDER_SWITCHING_KEYS
19log = logging.getLogger(__name__)
21_NO_FAILURE = object()
24class ConfigFileFollower:
25 """Keeps a running process current with config.toml, one check per request.
27 The check is two stat() calls against the last reading, off the loop and one at a
28 time: a request's check and the fleet's retry check share one thread lock. A change
29 goes through the same settle step as a write through the settings API, so this
30 process's own writes are not applied twice. A provider switch waits for a restart,
31 as the settings API refuses it here. A check that fails leaves the settings in
32 memory and the request runs; the error is logged once per file state.
33 """
35 def __init__(self) -> None:
36 self._reading: ConfigReading = start_config_reading()
37 self._lock = threading.Lock()
38 # The file state of the last failed check, so the error is logged once per state.
39 self._failed_stamp: object = _NO_FAILURE
41 def _adopt(self) -> SettingsUpdateResult | None:
42 """Take the outside changes; on an error, keep the settings in memory."""
43 with self._lock:
44 try:
45 self._reading, result = adopt_external_changes(
46 self._reading, hold=PROVIDER_SWITCHING_KEYS
47 )
48 except Exception:
49 stamp = persistent_settings.overlay_stamp(cfg.data_root)
50 if stamp != self._failed_stamp:
51 self._failed_stamp = stamp
52 log.exception(
53 "Could not take the changes in config.toml; "
54 "the server keeps its current settings."
55 )
56 return None
57 self._failed_stamp = _NO_FAILURE
58 return result
60 async def take(self) -> None:
61 """Take the changes another process made to config.toml, off the event loop."""
62 await asyncio.to_thread(self.take_now)
64 def take_now(self) -> None:
65 """Take the changes another process made to config.toml, and log what was taken."""
66 result = self._adopt()
67 if result is None:
68 return
69 log.info("Took %s from config.toml.", ", ".join(result.updated))
70 if result.reindex_required:
71 log.warning(
72 "config.toml changed a setting the library was built with; run lilbee rebuild."
73 )
74 for warning in result.warnings:
75 log.warning("%s", warning)