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

1"""Take what another process wrote to config.toml before a long-running process answers.""" 

2 

3from __future__ import annotations 

4 

5import asyncio 

6import logging 

7import threading 

8 

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 

18 

19log = logging.getLogger(__name__) 

20 

21_NO_FAILURE = object() 

22 

23 

24class ConfigFileFollower: 

25 """Keeps a running process current with config.toml, one check per request. 

26 

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 """ 

34 

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 

40 

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 

59 

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) 

63 

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)