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:
+7
-2
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user