ha(H1): ниты повторного ревью — гарантия destroy при повторном close, rate-limit warn

- close() из другого потока после close из reader-колбэка теперь джойнит
  reader (гарантия, что destroy завершён к возврату);
- предупреждения о переполнении очередей не чаще 1/30 с;
- events_dropped сохраняет последнее значение после close;
- find_library выбирает .so по версии (а не по длине имени).
This commit is contained in:
2026-09-29 11:55:36 +03:00
parent b84bd8d555
commit 397be6236d
2 changed files with 29 additions and 14 deletions
+7 -2
View File
@@ -206,12 +206,17 @@ def find_library() -> str:
return env return env
pkg = Path(__file__).resolve().parent pkg = Path(__file__).resolve().parent
pattern = re.compile(r"^libfgl-aircon\.(so(\.[0-9.]+)?|dylib|\d+\.dylib)$") pattern = re.compile(r"^libfgl-aircon\.(so(\.[0-9.]+)?|dylib|\d+\.dylib)$")
def version_key(path: Path) -> tuple[int, ...]:
match = re.search(r"\.so\.(\d+(?:\.\d+)*)$", path.name)
return tuple(int(part) for part in match.group(1).split(".")) if match else ()
candidates = sorted( candidates = sorted(
(p for p in pkg.iterdir() if p.is_file() and pattern.match(p.name)), (p for p in pkg.iterdir() if p.is_file() and pattern.match(p.name)),
key=lambda p: (p.name.count("."), len(p.name)), key=version_key,
) )
if candidates: if candidates:
return str(candidates[0]) return str(candidates[-1])
system = ctypes.util.find_library("fgl-aircon") system = ctypes.util.find_library("fgl-aircon")
if system: if system:
return system return system
+22 -12
View File
@@ -13,6 +13,7 @@ from __future__ import annotations
import asyncio import asyncio
import logging import logging
import threading import threading
import time
from dataclasses import dataclass from dataclasses import dataclass
from typing import Callable, Optional from typing import Callable, Optional
@@ -68,12 +69,16 @@ class _LogPump(threading.Thread):
def run(self) -> None: def run(self) -> None:
out = ffi.new("fgl_log_event_t[]", 8) out = ffi.new("fgl_log_event_t[]", 8)
dropped = 0 dropped = 0
warned_at = 0.0
while True: while True:
total = int(lib.fgl_log_events_dropped()) total = int(lib.fgl_log_events_dropped())
if total > dropped: if total > dropped:
_LOGGER.warning( if time.monotonic() - warned_at > 30.0:
"переполнение очереди логов: потеряно %d", total - dropped _LOGGER.warning(
) "переполнение очереди логов: потеряно %d",
total - dropped,
)
warned_at = time.monotonic()
dropped = total dropped = total
count = lib.fgl_log_wait_events(out, 8, 500) count = lib.fgl_log_wait_events(out, 8, 500)
handler = _GLOBAL_LOG handler = _GLOBAL_LOG
@@ -129,6 +134,7 @@ class _SessionReader(threading.Thread):
out = ffi.new("fgl_event_t[]", 16) out = ffi.new("fgl_event_t[]", 16)
finalize = ffi.NULL finalize = ffi.NULL
dropped = 0 dropped = 0
warned_at = 0.0
try: try:
while True: while True:
with self._session._lock: with self._session._lock:
@@ -143,11 +149,14 @@ class _SessionReader(threading.Thread):
if raw == ffi.NULL: if raw == ffi.NULL:
return return
total = int(lib.fgl_session_events_dropped(raw)) total = int(lib.fgl_session_events_dropped(raw))
self._session._dropped = total
if total > dropped: if total > dropped:
_LOGGER.warning( if time.monotonic() - warned_at > 30.0:
"переполнение очереди событий: потеряно %d", _LOGGER.warning(
total - dropped, "переполнение очереди событий: потеряно %d",
) total - dropped,
)
warned_at = time.monotonic()
dropped = total dropped = total
count = lib.fgl_session_wait_events(raw, out, 16, 250) count = lib.fgl_session_wait_events(raw, out, 16, 250)
for i in range(count): for i in range(count):
@@ -181,6 +190,7 @@ class Session:
self._loop = loop if loop is not None else _running_loop() self._loop = loop if loop is not None else _running_loop()
self._lock = threading.Lock() self._lock = threading.Lock()
self._closed = False self._closed = False
self._dropped = 0
self._cfg_strings: list = [] self._cfg_strings: list = []
@@ -275,8 +285,9 @@ class Session:
"""Сколько событий потеряно из-за переполнения очереди (диагностика).""" """Сколько событий потеряно из-за переполнения очереди (диагностика)."""
with self._lock: with self._lock:
if self._closed or self._session == ffi.NULL: if self._closed or self._session == ffi.NULL:
return 0 return self._dropped
return int(lib.fgl_session_events_dropped(self._session)) self._dropped = int(lib.fgl_session_events_dropped(self._session))
return self._dropped
@property @property
def config(self) -> Config: def config(self) -> Config:
@@ -296,11 +307,10 @@ class Session:
"""Останавливает сессию, джойнит reader и освобождает ядро. """Останавливает сессию, джойнит reader и освобождает ядро.
Из колбэка в reader-потоке (Session без loop) возвращается сразу: Из колбэка в reader-потоке (Session без loop) возвращается сразу:
освобождение завершит сам reader. Повторный вызов — no-op. освобождение завершит сам reader. Повторный вызов из другого потока
дожидается фактического освобождения (join идемпотентен).
""" """
with self._lock: with self._lock:
if self._closed:
return
self._closed = True self._closed = True
if self._reader is threading.current_thread(): if self._reader is threading.current_thread():
return return