fix(audit): threading.Lock on AuditLog.log + close (P1 bug)
detection thread and async heartbeat call log() concurrently. Without a lock, two threads can both see today != _current_date and double-open the file, corrupting the handle. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import threading
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, IO
|
from typing import Callable, IO
|
||||||
@@ -16,28 +17,28 @@ class AuditLog:
|
|||||||
self._clock: Callable[[], datetime] = clock or datetime.now
|
self._clock: Callable[[], datetime] = clock or datetime.now
|
||||||
self._current_date: date | None = None
|
self._current_date: date | None = None
|
||||||
self._fh: IO[str] | None = None
|
self._fh: IO[str] | None = None
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
def log(self, event: dict) -> None:
|
def log(self, event: dict) -> None:
|
||||||
now = self._clock()
|
now = self._clock()
|
||||||
today = now.date()
|
today = now.date()
|
||||||
|
|
||||||
if today != self._current_date:
|
|
||||||
self._open(today)
|
|
||||||
|
|
||||||
if "ts" not in event:
|
if "ts" not in event:
|
||||||
event = {**event, "ts": now.isoformat()}
|
event = {**event, "ts": now.isoformat()}
|
||||||
|
with self._lock:
|
||||||
assert self._fh is not None
|
if today != self._current_date:
|
||||||
self._fh.write(json.dumps(event, separators=(",", ":")) + "\n")
|
self._open(today)
|
||||||
|
assert self._fh is not None
|
||||||
|
self._fh.write(json.dumps(event, separators=(",", ":")) + "\n")
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
if self._fh is not None:
|
with self._lock:
|
||||||
try:
|
if self._fh is not None:
|
||||||
self._fh.close()
|
try:
|
||||||
except Exception:
|
self._fh.close()
|
||||||
pass
|
except Exception:
|
||||||
finally:
|
pass
|
||||||
self._fh = None
|
finally:
|
||||||
|
self._fh = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_path(self) -> Path:
|
def current_path(self) -> Path:
|
||||||
|
|||||||
Reference in New Issue
Block a user