feat(alerts): publish reconciliation quantity mismatches to trex-notifier (req-0007/ac-0024)
Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
co-authored by
genichin
parent
baae02d2af
commit
153a563f6d
@@ -0,0 +1,122 @@
|
||||
"""PostgreSQL-backed durable reconcile notification lifecycle store (req-0007, ac-0024).
|
||||
|
||||
Tracks per-identity B/C-case mismatch notification fingerprints so that
|
||||
duplicate alarms are suppressed across scheduler ticks and process restarts,
|
||||
and pending resolved transitions can be retried.
|
||||
|
||||
PK: identity_key (slot_id for B-case; "{instance_key}\\x00C\\x00{symbol}" for C-case).
|
||||
lifecycle_state: 'notified' | 'pending_resolved'.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from trex_core.adapters.persistence.postgresql import _require_psycopg
|
||||
|
||||
|
||||
class PostgreSQLReconcileNotificationStore:
|
||||
"""`reconcile_notification_state` table CRUD (req-0007, ac-0024)."""
|
||||
|
||||
def __init__(self, dsn: str, *, psycopg_module: Any = None) -> None:
|
||||
self._psycopg = _require_psycopg(psycopg_module)
|
||||
self._dsn = dsn
|
||||
|
||||
def get_state(self, identity_key: str) -> dict | None:
|
||||
"""Return state dict or None."""
|
||||
with self._psycopg.connect(self._dsn) as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT identity_key, case_code, instance_key, symbol, "
|
||||
" quantity_a, quantity_b, lifecycle_state "
|
||||
"FROM reconcile_notification_state "
|
||||
"WHERE identity_key = %s LIMIT 1",
|
||||
(identity_key,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
return {
|
||||
"identity_key": row[0],
|
||||
"case_code": row[1],
|
||||
"instance_key": row[2],
|
||||
"symbol": row[3],
|
||||
"quantity_a": row[4],
|
||||
"quantity_b": row[5],
|
||||
"lifecycle_state": row[6],
|
||||
}
|
||||
|
||||
def set_state(
|
||||
self,
|
||||
identity_key: str,
|
||||
*,
|
||||
case_code: str,
|
||||
instance_key: str,
|
||||
symbol: str,
|
||||
quantity_a: int,
|
||||
quantity_b: int,
|
||||
lifecycle_state: str,
|
||||
) -> None:
|
||||
"""Upsert notification state."""
|
||||
now = datetime.now(tz=UTC)
|
||||
with self._psycopg.connect(self._dsn, autocommit=False) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO reconcile_notification_state "
|
||||
"(identity_key, case_code, instance_key, symbol, "
|
||||
" quantity_a, quantity_b, lifecycle_state, created_at, updated_at) "
|
||||
"VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) "
|
||||
"ON CONFLICT (identity_key) DO UPDATE "
|
||||
"SET case_code = EXCLUDED.case_code, "
|
||||
" instance_key = EXCLUDED.instance_key, "
|
||||
" symbol = EXCLUDED.symbol, "
|
||||
" quantity_a = EXCLUDED.quantity_a, "
|
||||
" quantity_b = EXCLUDED.quantity_b, "
|
||||
" lifecycle_state = EXCLUDED.lifecycle_state, "
|
||||
" updated_at = EXCLUDED.updated_at",
|
||||
(
|
||||
identity_key,
|
||||
case_code,
|
||||
instance_key,
|
||||
symbol,
|
||||
quantity_a,
|
||||
quantity_b,
|
||||
lifecycle_state,
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def clear_state(self, identity_key: str) -> None:
|
||||
"""Delete state row (called after successful resolved publish)."""
|
||||
with self._psycopg.connect(self._dsn, autocommit=False) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"DELETE FROM reconcile_notification_state WHERE identity_key = %s",
|
||||
(identity_key,),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def list_states_by_lifecycle(self, lifecycle_state: str) -> list[dict]:
|
||||
"""Return all state dicts with the given lifecycle_state."""
|
||||
with self._psycopg.connect(self._dsn) as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT identity_key, case_code, instance_key, symbol, "
|
||||
" quantity_a, quantity_b, lifecycle_state "
|
||||
"FROM reconcile_notification_state "
|
||||
"WHERE lifecycle_state = %s",
|
||||
(lifecycle_state,),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
return [
|
||||
{
|
||||
"identity_key": row[0],
|
||||
"case_code": row[1],
|
||||
"instance_key": row[2],
|
||||
"symbol": row[3],
|
||||
"quantity_a": row[4],
|
||||
"quantity_b": row[5],
|
||||
"lifecycle_state": row[6],
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
Reference in New Issue
Block a user