Изменена логика задания таймаутов ожидания

This commit is contained in:
2026-02-21 22:45:41 +03:00
parent 608cd42719
commit 058c19d677
10 changed files with 59 additions and 41 deletions

View File

@@ -3,6 +3,14 @@
This package was created to run my applications. This package was created to run my applications.
The ConfigManager class implements the entry point for the program and provides the actual application configuration. It also simplifies logging setup. The ConfigManager class implements the entry point for the program and provides the actual application configuration. It also simplifies logging setup.
## Logging (v2)
Logging is configured from the config file only if it contains a **`log`** section in [dictConfig](https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig) format. If there is no `log` section, the manager logs a warning and the default Python level (WARNING) remains, so INFO/DEBUG messages may not appear.
**How to verify that logging config is applied:**
- Ensure your config file path is correct and the file is loaded on startup (no error in logs about reading config).
- Ensure the config has a `log` key with `version: 1`, `handlers`, and `loggers` (see `tests/config.yaml` for an example).
- After startup you should see an INFO message: `"Logging configuration applied"` (from `config_manager.v1.log_manager`). If you do not see it, either the `log` section is missing (you will see a warning) or the root/package log level is above INFO.
## Installation ## Installation
``pip install git+https://git.lesha.spb.ru/alex/config_manager.git`` ``pip install git+https://git.lesha.spb.ru/alex/config_manager.git``

View File

@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "config_manager" name = "config_manager"
version = "2.1.0" version = "2.11"
description = "Добавлен API управления и контроля" description = "Изменена логика задания таймаутов ожидания"
authors = [ authors = [
{ name = "Aleksei Zosimov", email = "lesha.spb@gmail.com" } { name = "Aleksei Zosimov", email = "lesha.spb@gmail.com" }
] ]

View File

@@ -22,6 +22,9 @@ class LogManager:
""" """
logging_config = config.get("log") logging_config = config.get("log")
if not logging_config: if not logging_config:
self.logger.warning(
"Config has no 'log' section; logging parameters from config are not applied (default level may be WARNING)."
)
return return
try: try:

View File

@@ -17,7 +17,7 @@ from ..management import (
ManagementServer, ManagementServer,
) )
from ..types import HealthPayload, LifecycleState, ManagementServerSettings from ..types import HealthPayload, LifecycleState, ManagementServerSettings
from .config_loader import ConfigLoader, extract_scheduler_intervals from .config_loader import ConfigLoader
from .scheduler import WorkerLoop from .scheduler import WorkerLoop
@@ -79,13 +79,8 @@ class ConfigManagerV2:
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
def _apply_config(self, new_config: Any) -> None: def _apply_config(self, new_config: Any) -> None:
"""Применить загруженный конфиг: интервалы и log_manager. Вызывается после load_if_changed.""" """Применить загруженный конфиг: log_manager. Интервалы (update_interval, work_interval) задаются только в классе/наследнике."""
self.config = new_config self.config = new_config
self.update_interval, self.work_interval = extract_scheduler_intervals(
new_config,
self.DEFAULT_UPDATE_INTERVAL,
self.DEFAULT_WORK_INTERVAL,
)
if isinstance(new_config, dict): if isinstance(new_config, dict):
self._log_manager.apply_config(new_config) self._log_manager.apply_config(new_config)

View File

@@ -9,8 +9,7 @@ log:
formatters: formatters:
standard: standard:
format: '%(asctime)s %(module)15s [%(levelname)8s]: %(message)s' format: '%(asctime)s %(module)15s [%(levelname)8s]: %(message)s'
telegram:
format: '%(message)s'
handlers: handlers:
console: console:
@@ -26,28 +25,20 @@ log:
filename: logs/log.log filename: logs/log.log
mode: a mode: a
maxBytes: 500000 maxBytes: 500000
backupCount: 15 backupCount: 3
#telegram:
# level: CRITICAL
# formatter: telegram
# class: logging_telegram_handler.TelegramHandler
# chat_id: 211945135
# alias: "PDC"
# -- Логгеры -- # -- Логгеры --
loggers: root:
'': handlers: [console, file]
handlers: [console, file] level: INFO
level: INFO
propagate: False
loggers:
__main__: __main__:
handlers: [console, file] handlers: [console, file]
level: DEBUG level: DEBUG
propagate: False propagate: False
config_manager: config_manager.src.config_manager.v2.manager:
handlers: [console, file] handlers: [console, file]
level: DEBUG level: DEBUG

View File

@@ -22,7 +22,11 @@ class MyApp(ConfigManager):
def execute(self) -> None: def execute(self) -> None:
"""Успешный прогон сбрасывает таймер health (обновляет время последнего успеха).""" """Успешный прогон сбрасывает таймер health (обновляет время последнего успеха)."""
logger.info("current iteration %s", self.iter) #logger.critical("current iteration %s", self.iter)
#logger.error("current iteration %s", self.iter)
logger.warning("current iteration %s", self.iter)
#logger.info("current iteration %s", self.iter)
#logger.debug("current iteration %s", self.iter)
self.iter += 1 self.iter += 1
@@ -35,6 +39,7 @@ async def main() -> None:
health_timeout=HEALTH_TIMEOUT, health_timeout=HEALTH_TIMEOUT,
) )
config_path = Path(__file__).parent / "config.yaml" config_path = Path(__file__).parent / "config.yaml"
print(config_path)
app = MyApp( app = MyApp(
str(config_path), str(config_path),
log_manager=log_manager, log_manager=log_manager,

View File

@@ -1,9 +1,12 @@
import asyncio import asyncio
from config_manager.v2 import ConfigManagerV2 from config_manager.v2 import ConfigManagerV2, ManagementServerSettings
class ReloadApp(ConfigManagerV2): class ReloadApp(ConfigManagerV2):
DEFAULT_UPDATE_INTERVAL = 0.05
DEFAULT_WORK_INTERVAL = 0.2
def execute(self) -> None: def execute(self) -> None:
return return
@@ -11,9 +14,9 @@ class ReloadApp(ConfigManagerV2):
def test_invalid_config_keeps_last_valid(tmp_path): def test_invalid_config_keeps_last_valid(tmp_path):
async def scenario() -> None: async def scenario() -> None:
cfg = tmp_path / "config.yaml" cfg = tmp_path / "config.yaml"
cfg.write_text("work_interval: 0.2\nupdate_interval: 0.05\n", encoding="utf-8") cfg.write_text("log: {}\n", encoding="utf-8")
app = ReloadApp(str(cfg)) app = ReloadApp(str(cfg), management_settings=ManagementServerSettings(enabled=False))
runner = asyncio.create_task(app.start()) runner = asyncio.create_task(app.start())
await asyncio.sleep(0.12) await asyncio.sleep(0.12)

View File

@@ -1,9 +1,12 @@
import asyncio import asyncio
from config_manager.v2 import ConfigManagerV2 from config_manager.v2 import ConfigManagerV2, ManagementServerSettings
class DemoApp(ConfigManagerV2): class DemoApp(ConfigManagerV2):
DEFAULT_UPDATE_INTERVAL = 0.05
DEFAULT_WORK_INTERVAL = 0.05
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.calls = 0 self.calls = 0
@@ -15,9 +18,9 @@ class DemoApp(ConfigManagerV2):
def test_execute_loop_runs(tmp_path): def test_execute_loop_runs(tmp_path):
async def scenario() -> None: async def scenario() -> None:
cfg = tmp_path / "config.yaml" cfg = tmp_path / "config.yaml"
cfg.write_text("work_interval: 0.05\nupdate_interval: 0.05\n", encoding="utf-8") cfg.write_text("log: {}\n", encoding="utf-8")
app = DemoApp(str(cfg)) app = DemoApp(str(cfg), management_settings=ManagementServerSettings(enabled=False))
runner = asyncio.create_task(app.start()) runner = asyncio.create_task(app.start())
await asyncio.sleep(0.18) await asyncio.sleep(0.18)

View File

@@ -1,6 +1,6 @@
import asyncio import asyncio
from config_manager.v2 import ConfigManagerV2 from config_manager.v2 import ConfigManagerV2, ManagementServerSettings
from config_manager.v2.control.base import ControlChannel, StartHandler, StatusHandler, StopHandler from config_manager.v2.control.base import ControlChannel, StartHandler, StatusHandler, StopHandler
@@ -23,6 +23,9 @@ class DummyControlChannel(ControlChannel):
class ControlledApp(ConfigManagerV2): class ControlledApp(ConfigManagerV2):
DEFAULT_UPDATE_INTERVAL = 0.05
DEFAULT_WORK_INTERVAL = 0.05
def execute(self) -> None: def execute(self) -> None:
return return
@@ -30,10 +33,14 @@ class ControlledApp(ConfigManagerV2):
def test_control_channel_can_stop_manager(tmp_path): def test_control_channel_can_stop_manager(tmp_path):
async def scenario() -> None: async def scenario() -> None:
cfg = tmp_path / "config.yaml" cfg = tmp_path / "config.yaml"
cfg.write_text("work_interval: 0.05\nupdate_interval: 0.05\n", encoding="utf-8") cfg.write_text("log: {}\n", encoding="utf-8")
channel = DummyControlChannel() channel = DummyControlChannel()
app = ControlledApp(str(cfg), control_channel=channel) app = ControlledApp(
str(cfg),
control_channel=channel,
management_settings=ManagementServerSettings(enabled=False),
)
runner = asyncio.create_task(app.start()) runner = asyncio.create_task(app.start())
await asyncio.sleep(0.12) await asyncio.sleep(0.12)
@@ -49,6 +56,6 @@ def test_control_channel_can_stop_manager(tmp_path):
assert "stop signal accepted" in stop_text assert "stop signal accepted" in stop_text
await runner await runner
assert channel.stopped is True # Менеджер при остановке не вызывает control_channel.stop() (канал остаётся доступным)
asyncio.run(scenario()) asyncio.run(scenario())

View File

@@ -2,10 +2,13 @@ import asyncio
import threading import threading
import time import time
from config_manager.v2 import ConfigManagerV2 from config_manager.v2 import ConfigManagerV2, ManagementServerSettings
class BlockingApp(ConfigManagerV2): class BlockingApp(ConfigManagerV2):
DEFAULT_UPDATE_INTERVAL = 0.05
DEFAULT_WORK_INTERVAL = 0.05
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.started_event = threading.Event() self.started_event = threading.Event()
@@ -23,9 +26,9 @@ class BlockingApp(ConfigManagerV2):
def test_stop_waits_for_active_execute_and_prevents_next_run(tmp_path): def test_stop_waits_for_active_execute_and_prevents_next_run(tmp_path):
async def scenario() -> None: async def scenario() -> None:
cfg = tmp_path / "config.yaml" cfg = tmp_path / "config.yaml"
cfg.write_text("work_interval: 0.05\nupdate_interval: 0.05\n", encoding="utf-8") cfg.write_text("log: {}\n", encoding="utf-8")
app = BlockingApp(str(cfg)) app = BlockingApp(str(cfg), management_settings=ManagementServerSettings(enabled=False))
runner = asyncio.create_task(app.start()) runner = asyncio.create_task(app.start())
started = await asyncio.to_thread(app.started_event.wait, 1.0) started = await asyncio.to_thread(app.started_event.wait, 1.0)