fix(agents): use in-memory PK pools for DML (avoid full-table scans on 54M-row tables)

This commit is contained in:
mo
2026-06-27 01:27:56 +02:00
parent 3c9661e7f9
commit 5147538b05
+57 -47
View File
@@ -23,8 +23,8 @@ from __future__ import annotations
import asyncio import asyncio
import os import os
import random import random
import time
import uuid import uuid
from collections import deque
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any from typing import Any
@@ -118,14 +118,34 @@ def _tag(run_id: str) -> str:
return f"{AGENT_TAG} {run_id}" return f"{AGENT_TAG} {run_id}"
# In-memory pools of agent-created primary keys per source. All UPDATE/DELETE
# operate by PK on these (instant), so we never scan the large unindexed
# `notes` column. Bounded by _POOL_CAP; orphaned tagged rows after a restart
# are harmless (clearly marked) and simply not re-tracked.
_pools: dict[str, deque] = {
"postgres": deque(maxlen=_POOL_CAP),
"mysql": deque(maxlen=_POOL_CAP),
"mongodb": deque(maxlen=_POOL_CAP),
}
def _resolve_op(op: str, pool: deque) -> str:
"""UPDATE/DELETE require an existing agent row; otherwise fall back to INSERT."""
if op in ("update", "delete") and not pool:
return "insert"
if op == "insert" and len(pool) >= _POOL_CAP:
return "delete" # keep the agent footprint bounded
return op
def _pg_dml(op: str) -> str: def _pg_dml(op: str) -> str:
pool = _pools["postgres"]
op = _resolve_op(op, pool)
conn = _pg_conn() conn = _pg_conn()
try: try:
conn.autocommit = True conn.autocommit = True
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute("SELECT count(*) FROM public.sales_orders WHERE notes LIKE %s", (AGENT_TAG + "%",)) if op == "insert":
pool = cur.fetchone()[0]
if op == "insert" or (op == "delete" and pool == 0) or (op == "update" and pool == 0):
rid = uuid.uuid4().hex[:8] rid = uuid.uuid4().hex[:8]
cur.execute( cur.execute(
"""INSERT INTO public.sales_orders """INSERT INTO public.sales_orders
@@ -139,38 +159,29 @@ def _pg_dml(op: str) -> str:
), ),
) )
oid = cur.fetchone()[0] oid = cur.fetchone()[0]
pool.append(oid)
return f"INSERT sales_orders order_id={oid} ({_tag(rid)})" return f"INSERT sales_orders order_id={oid} ({_tag(rid)})"
if op == "update": if op == "update":
oid = random.choice(list(pool))
cur.execute( cur.execute(
"""UPDATE public.sales_orders SET order_status=%s, amount=round(amount*%s,2) "UPDATE public.sales_orders SET order_status=%s, amount=round(amount*%s,2) WHERE order_id=%s",
WHERE order_id IN ( (random.choice(ORDER_STATUS), round(random.uniform(0.9, 1.2), 2), oid),
SELECT order_id FROM public.sales_orders WHERE notes LIKE %s
ORDER BY order_id DESC LIMIT 1)
RETURNING order_id""",
(random.choice(ORDER_STATUS), round(random.uniform(0.9, 1.2), 2), AGENT_TAG + "%"),
) )
row = cur.fetchone() return f"UPDATE sales_orders order_id={oid}"
return f"UPDATE sales_orders order_id={row[0]}" if row else "UPDATE sales_orders (no agent rows)" oid = pool.popleft()
# delete cur.execute("DELETE FROM public.sales_orders WHERE order_id=%s", (oid,))
cur.execute( return f"DELETE sales_orders order_id={oid}"
"""DELETE FROM public.sales_orders WHERE order_id IN (
SELECT order_id FROM public.sales_orders WHERE notes LIKE %s
ORDER BY order_id ASC LIMIT 1) RETURNING order_id""",
(AGENT_TAG + "%",),
)
row = cur.fetchone()
return f"DELETE sales_orders order_id={row[0]}" if row else "DELETE sales_orders (no agent rows)"
finally: finally:
conn.close() conn.close()
def _mysql_dml(op: str) -> str: def _mysql_dml(op: str) -> str:
pool = _pools["mysql"]
op = _resolve_op(op, pool)
conn = _mysql_conn() conn = _mysql_conn()
try: try:
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute("SELECT count(*) FROM hr.employee_events WHERE notes LIKE %s", (AGENT_TAG + "%",)) if op == "insert":
pool = cur.fetchone()[0]
if op == "insert" or (op in ("update", "delete") and pool == 0):
rid = uuid.uuid4().hex[:8] rid = uuid.uuid4().hex[:8]
cur.execute( cur.execute(
"""INSERT INTO hr.employee_events """INSERT INTO hr.employee_events
@@ -182,28 +193,29 @@ def _mysql_dml(op: str) -> str:
round(random.uniform(-5000, 15000), 2), datetime.now(timezone.utc), _tag(rid), round(random.uniform(-5000, 15000), 2), datetime.now(timezone.utc), _tag(rid),
), ),
) )
return f"INSERT employee_events event_id={cur.lastrowid} ({_tag(rid)})" eid = cur.lastrowid
pool.append(eid)
return f"INSERT employee_events event_id={eid} ({_tag(rid)})"
if op == "update": if op == "update":
eid = random.choice(list(pool))
cur.execute( cur.execute(
"""UPDATE hr.employee_events SET salary_change=%s, event_type=%s "UPDATE hr.employee_events SET salary_change=%s, event_type=%s WHERE event_id=%s",
WHERE notes LIKE %s ORDER BY event_id DESC LIMIT 1""", (round(random.uniform(-5000, 15000), 2), random.choice(EVENT_TYPES), eid),
(round(random.uniform(-5000, 15000), 2), random.choice(EVENT_TYPES), AGENT_TAG + "%"),
) )
return f"UPDATE employee_events ({cur.rowcount} row)" return f"UPDATE employee_events event_id={eid}"
cur.execute( eid = pool.popleft()
"DELETE FROM hr.employee_events WHERE notes LIKE %s ORDER BY event_id ASC LIMIT 1", cur.execute("DELETE FROM hr.employee_events WHERE event_id=%s", (eid,))
(AGENT_TAG + "%",), return f"DELETE employee_events event_id={eid}"
)
return f"DELETE employee_events ({cur.rowcount} row)"
finally: finally:
conn.close() conn.close()
def _mongo_dml(op: str) -> str: def _mongo_dml(op: str) -> str:
pool = _pools["mongodb"]
op = _resolve_op(op, pool)
client, coll = _mongo_coll() client, coll = _mongo_coll()
try: try:
pool = coll.count_documents({"atc_agent": True}, limit=1) if op == "insert":
if op == "insert" or (op in ("update", "delete") and pool == 0):
rid = uuid.uuid4().hex[:8] rid = uuid.uuid4().hex[:8]
doc = { doc = {
"event_id": str(uuid.uuid4()), "event_id": str(uuid.uuid4()),
@@ -217,21 +229,18 @@ def _mongo_dml(op: str) -> str:
"agent_run": rid, "agent_run": rid,
} }
res = coll.insert_one(doc) res = coll.insert_one(doc)
pool.append(res.inserted_id)
return f"INSERT events _id={res.inserted_id} (agent_run={rid})" return f"INSERT events _id={res.inserted_id} (agent_run={rid})"
if op == "update": if op == "update":
doc = coll.find_one({"atc_agent": True}, sort=[("_id", -1)]) oid = random.choice(list(pool))
if not doc:
return "UPDATE events (no agent docs)"
coll.update_one( coll.update_one(
{"_id": doc["_id"]}, {"_id": oid},
{"$set": {"type": random.choice(MONGO_TYPES), "amount": round(random.uniform(10, 50000), 4)}}, {"$set": {"type": random.choice(MONGO_TYPES), "amount": round(random.uniform(10, 50000), 4)}},
) )
return f"UPDATE events _id={doc['_id']}" return f"UPDATE events _id={oid}"
doc = coll.find_one({"atc_agent": True}, sort=[("_id", 1)]) oid = pool.popleft()
if not doc: coll.delete_one({"_id": oid})
return "DELETE events (no agent docs)" return f"DELETE events _id={oid}"
coll.delete_one({"_id": doc["_id"]})
return f"DELETE events _id={doc['_id']}"
finally: finally:
client.close() client.close()
@@ -285,7 +294,8 @@ async def agent_dml_loop() -> None:
# ── Endpoints ──────────────────────────────────────────────────────────────── # ── Endpoints ────────────────────────────────────────────────────────────────
@router.get("/status") @router.get("/status")
async def status() -> JSONResponse: async def status() -> JSONResponse:
return JSONResponse({"ok": True, **_state}) pools = {k: len(v) for k, v in _pools.items()}
return JSONResponse({"ok": True, "pools": pools, **_state})
@router.post("/toggle") @router.post("/toggle")