feat: add authenticated engine processing boundary

This commit is contained in:
Repinoid
2026-10-31 17:36:55 +03:00
parent 193f0b4069
commit e2ba9ab451
9 changed files with 378 additions and 81 deletions
+14 -1
View File
@@ -12,7 +12,7 @@ import time
import os
from contextlib import contextmanager
DB_PATH = "/tmp/contracts.db"
DB_PATH = os.getenv("CONTRACTS_DB_PATH", "/tmp/contracts.db")
_db_session_key = None
_local = threading.local()
@@ -67,6 +67,19 @@ def _create_schema(conn):
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS extraction_jobs (
batch_id TEXT PRIMARY KEY,
owner TEXT NOT NULL,
state TEXT NOT NULL,
total INTEGER NOT NULL,
done INTEGER NOT NULL DEFAULT 0,
failed INTEGER NOT NULL DEFAULT 0,
classes_json TEXT NOT NULL DEFAULT '{}',
result_json TEXT,
error TEXT,
updated_at REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS contracts (
id TEXT PRIMARY KEY,
number TEXT NOT NULL,
+2 -3
View File
@@ -7,7 +7,7 @@
import json
import uuid
from db.connection import execute, query, transaction
from db.connection import execute, query
def _json_value(value, default):
@@ -123,5 +123,4 @@ def list_dossier_documents(dossier_id):
def clear_batch_dossiers(batch_id):
"""Удалить все досье batch; таблицы с ON DELETE CASCADE очистятся вместе с ними."""
with transaction():
return execute("DELETE FROM contract_dossiers WHERE batch_id = %s", (batch_id,))
return execute("DELETE FROM contract_dossiers WHERE batch_id = %s", (batch_id,))
+79
View File
@@ -0,0 +1,79 @@
"""Persistent extraction progress and per-batch worker ownership."""
import json
import time
import uuid
from db.connection import execute, query
LEASE_SECONDS = 300
def claim(batch_id, total):
"""Claim a batch atomically, allowing retry after failure or expired lease."""
owner = str(uuid.uuid4())
now = time.time()
changed = execute(
"""INSERT INTO extraction_jobs (batch_id, owner, state, total, updated_at)
VALUES (%s, %s, 'running', %s, %s)
ON CONFLICT(batch_id) DO UPDATE SET owner=excluded.owner, state='running',
total=excluded.total, done=0, failed=0, classes_json='{}',
result_json=NULL, error=NULL, updated_at=excluded.updated_at
WHERE extraction_jobs.state != 'running' OR extraction_jobs.updated_at < %s""",
(batch_id, owner, total, now, now - LEASE_SECONDS),
)
return owner if changed else None
def heartbeat(batch_id, owner):
return execute(
"UPDATE extraction_jobs SET updated_at=%s WHERE batch_id=%s AND owner=%s AND state='running'",
(time.time(), batch_id, owner),
)
def progress(batch_id, owner, completed, result):
rows = query(
"SELECT failed, classes_json FROM extraction_jobs WHERE batch_id=%s AND owner=%s AND state='running'",
(batch_id, owner),
)
if not rows:
return
failed = rows[0]["failed"]
classes = json.loads(rows[0]["classes_json"])
if not result.get("ok"):
failed += 1
else:
doc_class = (result.get("extraction") or {}).get("doc_class", "unknown")
classes[doc_class] = classes.get(doc_class, 0) + 1
execute(
"""UPDATE extraction_jobs SET done=%s, failed=%s, classes_json=%s, updated_at=%s
WHERE batch_id=%s AND owner=%s AND state='running'""",
(completed, failed, json.dumps(classes), time.time(), batch_id, owner),
)
def finish(batch_id, owner, result=None, error=None):
state = "done" if result and result.get("ok") and not error else "failed"
return execute(
"""UPDATE extraction_jobs SET state=%s, result_json=%s, error=%s, updated_at=%s
WHERE batch_id=%s AND owner=%s AND state='running'""",
(state, json.dumps(result, ensure_ascii=False) if result is not None else None,
error or ("extraction failed" if state == "failed" else None), time.time(), batch_id, owner),
)
def get(batch_id):
"""Mark an abandoned lease interrupted; callers may explicitly retry it."""
execute(
"""UPDATE extraction_jobs SET state='interrupted', error='worker stopped; retry the batch'
WHERE batch_id=%s AND state='running' AND updated_at < %s""",
(batch_id, time.time() - LEASE_SECONDS),
)
rows = query("SELECT * FROM extraction_jobs WHERE batch_id=%s", (batch_id,))
if not rows:
return None
job = rows[0]
job["classes"] = json.loads(job.pop("classes_json"))
result_json = job.pop("result_json")
job["result"] = json.loads(result_json) if result_json else None
return job