Аналитика снова пишет события в Postgres
При проверке на сервере выяснилось, что psycopg-соединение не умеет executemany: батч событий уходил в fallback и терялся. Запись переведена на курсор, добавлен тест на этот путь; версия 0.7.2.
This commit is contained in:
parent
cf14dfa80d
commit
448ab837fa
|
|
@ -171,7 +171,7 @@ pytest
|
||||||
|
|
||||||
## Версионирование
|
## Версионирование
|
||||||
|
|
||||||
- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.7.1`.
|
- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.7.2`.
|
||||||
- Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`).
|
- Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`).
|
||||||
- Docker-образы тегируются версией (Блок 2), события аналитики получают `bot_version`.
|
- Docker-образы тегируются версией (Блок 2), события аналитики получают `bot_version`.
|
||||||
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.
|
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,9 @@ Writer = Callable[[list[EventRow], MessageCounts], Awaitable[None]]
|
||||||
|
|
||||||
async def _write_db(events: list[EventRow], messages: MessageCounts) -> None:
|
async def _write_db(events: list[EventRow], messages: MessageCounts) -> None:
|
||||||
"""Записать батч в Postgres."""
|
"""Записать батч в Postgres."""
|
||||||
async with get_pool().connection() as conn:
|
async with get_pool().connection() as conn, conn.cursor() as cur:
|
||||||
if events:
|
if events:
|
||||||
await conn.executemany(
|
await cur.executemany(
|
||||||
"""
|
"""
|
||||||
INSERT INTO analytics.events
|
INSERT INTO analytics.events
|
||||||
(event_time, event_type, user_id, username, chat_id, payload, bot_version, prod)
|
(event_time, event_type, user_id, username, chat_id, payload, bot_version, prod)
|
||||||
|
|
@ -71,7 +71,7 @@ async def _write_db(events: list[EventRow], messages: MessageCounts) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
for (day, user_id, chat_id), (username, count) in messages.items():
|
for (day, user_id, chat_id), (username, count) in messages.items():
|
||||||
await conn.execute(
|
await cur.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO analytics.messages_daily (day, user_id, username, chat_id, count)
|
INSERT INTO analytics.messages_daily (day, user_id, username, chat_id, count)
|
||||||
VALUES (%s, %s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s)
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,4 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
__version__ = "0.7.1"
|
__version__ = "0.7.2"
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,51 @@ class FakeWriter:
|
||||||
self.messages[key] = list(value)
|
self.messages[key] = list(value)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeCursor:
|
||||||
|
"""Курсор psycopg: запоминает вызовы."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.calls: list[tuple[str, Any, Any]] = []
|
||||||
|
|
||||||
|
async def executemany(self, query: str, rows: list[Any]) -> None:
|
||||||
|
self.calls.append(("executemany", query, rows))
|
||||||
|
|
||||||
|
async def execute(self, query: str, params: Any) -> None:
|
||||||
|
self.calls.append(("execute", query, params))
|
||||||
|
|
||||||
|
async def __aenter__(self) -> FakeCursor:
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, *args: Any) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class FakeConnection:
|
||||||
|
"""Соединение psycopg с курсором."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.cursor_obj = FakeCursor()
|
||||||
|
|
||||||
|
def cursor(self) -> FakeCursor:
|
||||||
|
return self.cursor_obj
|
||||||
|
|
||||||
|
async def __aenter__(self) -> FakeConnection:
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, *args: Any) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class FakePool:
|
||||||
|
"""Пул с одним соединением."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.conn = FakeConnection()
|
||||||
|
|
||||||
|
def connection(self) -> FakeConnection:
|
||||||
|
return self.conn
|
||||||
|
|
||||||
|
|
||||||
def _settings(**overrides: Any) -> Settings:
|
def _settings(**overrides: Any) -> Settings:
|
||||||
return Settings(_env_file=None, bot_token="1:a", **overrides)
|
return Settings(_env_file=None, bot_token="1:a", **overrides)
|
||||||
|
|
||||||
|
|
@ -230,6 +275,26 @@ async def test_disabled_analytics_ignores_events() -> None:
|
||||||
assert fake.messages == {}
|
assert fake.messages == {}
|
||||||
|
|
||||||
|
|
||||||
|
# --- запись в Postgres ---
|
||||||
|
|
||||||
|
|
||||||
|
async def test_write_db_uses_cursor_executemany(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
from common.analytics import client as analytics_client
|
||||||
|
|
||||||
|
pool = FakePool()
|
||||||
|
monkeypatch.setattr(analytics_client, "get_pool", lambda: pool)
|
||||||
|
|
||||||
|
await analytics_client._write_db(
|
||||||
|
[[datetime.now(UTC), "command", 1, "vasya", 100, {"command": "инфо"}, "0.7.2", False]],
|
||||||
|
{(datetime.now(UTC).date(), 1, 100): ["vasya", 3]},
|
||||||
|
)
|
||||||
|
|
||||||
|
calls = pool.conn.cursor_obj.calls
|
||||||
|
assert calls[0][0] == "executemany"
|
||||||
|
assert calls[1][0] == "execute"
|
||||||
|
assert "messages_daily" in calls[1][1]
|
||||||
|
|
||||||
|
|
||||||
# --- деградация без Postgres ---
|
# --- деградация без Postgres ---
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,4 @@ from common import __version__
|
||||||
|
|
||||||
def test_version_is_set():
|
def test_version_is_set():
|
||||||
assert __version__
|
assert __version__
|
||||||
assert __version__ == "0.7.1"
|
assert __version__ == "0.7.2"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue