laya-ad / code /laya-server /backend /server /usage_writer.py
x6nux's picture
laya-ad v2.2: sync laya-server code (default version v2.2, build_push.sh, .env.example)
0cd599e verified
Raw History Blame Contribute Delete
5.57 kB
"""Bounded, best-effort usage telemetry; never part of inference success."""
import logging
import sqlite3
import threading
import time
from collections import deque
from dataclasses import dataclass
from .database import Database
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class UsageEvent:
created_at: str
source: str
key_id: int | None
model: str
input_tokens: int
output_tokens: int
duration_ms: int
class UsageWriter:
def __init__(self, db: Database, *, capacity: int = 1000, batch_size: int = 100,
flush_interval: float = 1.0, shutdown_timeout: float = 5.0):
if min(capacity, batch_size, flush_interval, shutdown_timeout) <= 0:
raise ValueError("Usage writer limits must be positive")
self.db = db
self.capacity = capacity
self.batch_size = batch_size
self.flush_interval = flush_interval
self.shutdown_timeout = shutdown_timeout
self._condition = threading.Condition()
self._pending: deque[UsageEvent] = deque()
self._thread: threading.Thread | None = None
self._accepting = False
self._deadline: float | None = None
self.dropped = 0
def start(self) -> None:
with self._condition:
if self._thread is not None and self._thread.is_alive():
raise RuntimeError("Previous usage writer is still running")
self._deadline = None
self._accepting = True
self._thread = threading.Thread(target=self._run, name="laya-usage-writer", daemon=True)
self._thread.start()
def submit(self, event: UsageEvent) -> bool:
with self._condition:
if not self._accepting or len(self._pending) >= self.capacity:
self.dropped += 1
# Avoid flooding logs when storage cannot keep up.
if self.dropped == 1 or self.dropped & (self.dropped - 1) == 0:
logger.warning("Usage queue unavailable/full; dropped events=%d", self.dropped)
return False
self._pending.append(event)
self._condition.notify()
return True
def stop(self) -> None:
with self._condition:
self._accepting = False
self._deadline = time.monotonic() + self.shutdown_timeout
self._condition.notify_all()
if self._thread is not None:
self._thread.join(self.shutdown_timeout)
if self._thread.is_alive():
logger.warning("Usage shutdown timed out; unflushed statistics may be lost")
def _next_batch(self) -> list[UsageEvent]:
with self._condition:
while not self._pending and self._accepting:
self._condition.wait()
flush_at = time.monotonic() + self.flush_interval
while self._accepting and len(self._pending) < self.batch_size:
remaining = flush_at - time.monotonic()
if remaining <= 0:
break
self._condition.wait(remaining)
if self._deadline is not None and time.monotonic() >= self._deadline:
self.dropped += len(self._pending)
self._pending.clear()
return [self._pending.popleft() for _ in range(min(self.batch_size, len(self._pending)))]
@staticmethod
def _write_batch(connection: sqlite3.Connection, batch: list[UsageEvent]) -> None:
latest: dict[int, str] = {}
for event in batch:
if event.key_id is not None:
latest[event.key_id] = max(latest.get(event.key_id, ""), event.created_at)
with connection:
connection.executemany(
"INSERT INTO usage_events (created_at,source,key_id,model,input_tokens,output_tokens,duration_ms) "
"VALUES (?,?,?,?,?,?,?)",
[(e.created_at, e.source, e.key_id, e.model, e.input_tokens, e.output_tokens, e.duration_ms)
for e in batch],
)
connection.executemany(
"UPDATE api_keys SET last_used_at=MAX(COALESCE(last_used_at,''),?) WHERE id=?",
[(created_at, key_id) for key_id, created_at in latest.items()],
)
def _run(self) -> None:
connection = None
try:
while batch := self._next_batch():
try:
if connection is None:
connection = self.db.open_connection()
self._write_batch(connection, batch)
except sqlite3.Error:
with self._condition:
self.dropped += len(batch)
logger.exception("Failed to persist %d usage events; dropping batch", len(batch))
# Do not retry an ambiguously committed batch and double-count it.
if connection is not None:
connection.close()
connection = None
finally:
with self._condition:
self._accepting = False
if self._pending:
self.dropped += len(self._pending)
logger.warning("Usage writer stopped; discarding %d pending events", len(self._pending))
self._pending.clear()
if connection is not None:
connection.close()