feat: central RTC write modes (DISABLED/DRY_RUN/APPROVAL/ENABLED), R4 suggestion-only, preview/approval, per-record rollback, bulk blocker upsert; 1.2.0
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+135
-52
@@ -1,19 +1,23 @@
|
||||
"""COPY-ONLY write E2E for the RTC auto-apply (C000025.CCLASSTRIB).
|
||||
"""COPY-ONLY write E2E for the centrally controlled RTC auto-apply (C000025.CCLASSTRIB).
|
||||
|
||||
python scripts/rtc_copy_write_e2e.py
|
||||
|
||||
Runs ONLY against scratch_rtc_e2e/TESTESOFICIAIS_RTC_COPY.FDB (a gbak restore of TESTESOFICIAIS). It refuses any other path, and refuses every real database.
|
||||
Scenario (all through the Agent's own code path `run_agent_service._rtc_auto_apply`):
|
||||
1. the copy's company is switched to regime normal (C000004.CRT='3'): a disposable experiment, so the rules can act (the real company is Simples => 0 candidates);
|
||||
2. assess -> candidates; 3. apply with all three gates open; 4. re-read, FK/CST checks, coverage; 5. ONLY CCLASSTRIB changed (whole-table diff);
|
||||
6. second run: 0 candidates, 0 writes; 7. stale-decision abort (RTC switched off on the copy between decision and write) writes nothing.
|
||||
Runs ONLY against scratch_rtc_e2e/TESTESOFICIAIS_RTC_COPY.FDB (restored from its own .fbk at the start, so every run is reproducible). It refuses any other path and every
|
||||
real database. Everything goes through the Agent's own code path `run_agent_service._rtc_auto_apply`, driven ONLY by the authenticated job `authorizations` (no env switches):
|
||||
0 restore + company -> regime normal (documented experiment on the copy); 1 whole-database baseline hashes;
|
||||
2 DISABLED (0 writes, no preview) / DRY_RUN (preview, 0 writes) / stale decision (RTC off between decision and write => 0 writes);
|
||||
3 APPROVAL_REQUIRED with 5 approved products (only those 5 written) / then ENABLED until nothing is left;
|
||||
4 reread, FK + CST integrity, coverage; 5 second run = 0 writes;
|
||||
6 rollback of 3 audit records (+1 conflict: value changed by an operator => refused); 7 reread, reapply, idempotency;
|
||||
8 invariants: NON_TARGET_COLUMNS_CHANGED, CATALOG_CHANGED, PARAMETERS_CHANGED, DOCUMENT_TABLES_CHANGED, OTHER_TABLES_CHANGED all 0.
|
||||
Writes go to the copy and nowhere else.
|
||||
"""
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -27,7 +31,9 @@ from central.app.operational_state import classify_product_state, summarize_oper
|
||||
from gti_fiscal_agent import firebird_reader, unified_fiscal # noqa: E402
|
||||
|
||||
COPY = str((ROOT / "scratch_rtc_e2e" / "TESTESOFICIAIS_RTC_COPY.FDB").resolve())
|
||||
FBK = str((ROOT / "scratch_rtc_e2e" / "TESTESOFICIAIS_RTC_COPY.fbk").resolve())
|
||||
NCM_OK = set((ROOT / "docs" / "runs" / "ncm_valid_codes.txt").read_text().split())
|
||||
DOC_TABLE_HINTS = ("NFCE", "NFE", "NF_", "CUPOM", "VENDA", "CTE", "MDFE")
|
||||
|
||||
|
||||
def guard(path):
|
||||
@@ -58,82 +64,159 @@ def digits(v):
|
||||
return "".join(ch for ch in str(v or "") if ch.isdigit())
|
||||
|
||||
|
||||
def snapshot_table(cur, table, key, skip_cols=()):
|
||||
def restore_copy():
|
||||
guard(COPY)
|
||||
gbak = Path(os.environ["FIREBIRD_CLIENT_LIBRARY"]).parent / "gbak.exe"
|
||||
done = subprocess.run([str(gbak), "-c", "-rep", "-n", "-user", os.environ["FB_USER"], "-pas", os.environ["FB_PASSWORD"], FBK, COPY], capture_output=True, text=True, timeout=600)
|
||||
if done.returncode != 0:
|
||||
raise SystemExit(f"restore failed: {done.stderr[-300:]}")
|
||||
|
||||
|
||||
def tables(cur):
|
||||
cur.execute("SELECT TRIM(RDB$RELATION_NAME) FROM RDB$RELATIONS WHERE COALESCE(RDB$SYSTEM_FLAG, 0) = 0 AND RDB$VIEW_BLR IS NULL AND RDB$EXTERNAL_FILE IS NULL ORDER BY 1")
|
||||
return [r[0] for r in cur.fetchall()]
|
||||
|
||||
|
||||
def table_hash(cur, table, skip_cols=()):
|
||||
cur.execute(f"SELECT * FROM {table}")
|
||||
cols = [d[0] for d in cur.description]
|
||||
ki = cols.index(key)
|
||||
return cols, {r[ki]: {c: v for c, v in zip(cols, r) if c not in skip_cols} for r in cur.fetchall()}
|
||||
keep = [i for i, c in enumerate(cols) if c not in skip_cols]
|
||||
digest = hashlib.sha256()
|
||||
rows = 0
|
||||
for row in cur.fetchall():
|
||||
digest.update(repr([row[i] for i in keep]).encode("utf-8", "replace"))
|
||||
rows += 1
|
||||
return f"{rows}:{digest.hexdigest()[:20]}"
|
||||
|
||||
|
||||
def coverage(env, company, ncm_valid, override=None):
|
||||
def baseline():
|
||||
conn = connect(False); cur = conn.cursor()
|
||||
out = {t: table_hash(cur, t, skip_cols=("CCLASSTRIB",) if t == "C000025" else ()) for t in tables(cur)}
|
||||
cur.execute("SELECT CODIGO, CCLASSTRIB FROM C000025")
|
||||
values = {str(r[0]).strip(): (r[1] or "").strip() for r in cur.fetchall()}
|
||||
conn.close()
|
||||
return out, values
|
||||
|
||||
|
||||
def diff_tables(before, after):
|
||||
return sorted(t for t in before if before[t] != after.get(t))
|
||||
|
||||
|
||||
def coverage(env, company, ncm_valid):
|
||||
conn = firebird_reader._connect(env)
|
||||
try:
|
||||
out = unified_fiscal.assess_from_connection(conn, company, ncm_valid=ncm_valid, rtc_override=override)
|
||||
out = unified_fiscal.assess_from_connection(conn, company, ncm_valid=ncm_valid)
|
||||
finally:
|
||||
conn.close()
|
||||
products = firebird_reader.read_products(env)
|
||||
states = [classify_product_state(p, {"status": "EXACT_NCM"}, fiscal=out["findings"].get(str(p["CODIGO"]))) for p in products]
|
||||
s = summarize_operational_states(states)
|
||||
return out, {"total": s["TOTAL_PRODUCTS"], "complete": s["COMPLETE_PRODUCTS"], "blocked": s["SAFE_BLOCKED"], "unclassified": s["UNCLASSIFIED"], "coverage_percent": round(100 * s["COMPLETE_PRODUCTS"] / s["TOTAL_PRODUCTS"], 2)}
|
||||
return out, products, {"total": s["TOTAL_PRODUCTS"], "complete": s["COMPLETE_PRODUCTS"], "blocked": s["SAFE_BLOCKED"], "unclassified": s["UNCLASSIFIED"], "coverage_percent": round(100 * s["COMPLETE_PRODUCTS"] / s["TOTAL_PRODUCTS"], 2)}
|
||||
|
||||
|
||||
def main():
|
||||
load_dotenv(ROOT / ".env") # credentials only when the script runs, never as an import side effect
|
||||
guard(COPY)
|
||||
service = load_service()
|
||||
restore_copy()
|
||||
env = {"FIREBIRD_DATABASE_PATH": COPY, "FB_USER": os.environ["FB_USER"], "FB_PASSWORD": os.environ["FB_PASSWORD"], "FIREBIRD_CLIENT_LIBRARY": os.environ.get("FIREBIRD_CLIENT_LIBRARY")}
|
||||
report = {"database": "scratch_rtc_e2e/TESTESOFICIAIS_RTC_COPY.FDB (gbak restore of TESTESOFICIAIS)", "real_database_writes": 0, "real_database_write_attempts": 0}
|
||||
# ---- 1. company -> regime normal on the COPY (documented experiment)
|
||||
report = {"database": "scratch_rtc_e2e/TESTESOFICIAIS_RTC_COPY.FDB (restored from its .fbk at start)", "real_database_writes": 0, "real_database_write_attempts": 0, "mechanism": "job authorizations only (no env switches)"}
|
||||
rw = connect(True); cur = rw.cursor()
|
||||
cur.execute("SELECT DISTINCT TRIM(CRT) FROM C000004"); report["copy_original_crt"] = [r[0] for r in cur.fetchall()]
|
||||
cur.execute("UPDATE C000004 SET CRT = '3'"); rw.commit()
|
||||
cur.execute("SELECT DISTINCT TRIM(CRT) FROM C000004"); company = {"crt": cur.fetchone()[0]}
|
||||
report["copy_crt_for_experiment"] = company["crt"]
|
||||
_, before_c25 = snapshot_table(cur, "C000025", "CODIGO")
|
||||
cur.execute("SELECT USAR_REFORMA_TRIBUTARIA FROM PARAMETROS"); report["copy_rtc_param"] = cur.fetchone()[0]
|
||||
cur.execute("SELECT CODIGO, CLASSIFICACAO_FISCAL FROM C000025"); ncm_valid = {str(c).strip(): digits(n) in NCM_OK for c, n in cur.fetchall()}
|
||||
rw.commit(); rw.close()
|
||||
ncm_valid = {str(c).strip(): digits(v["CLASSIFICACAO_FISCAL"]) in NCM_OK for c, v in before_c25.items()}
|
||||
# ---- 2. assess (as found on the copy)
|
||||
out, cov_before = coverage(env, company, ncm_valid)
|
||||
report["copy_crt_for_experiment"] = company["crt"]
|
||||
base, base_values = baseline()
|
||||
report["tables_hashed"] = len(base)
|
||||
config = SimpleNamespace(rtc_auto_apply_max_per_job=500, database_path=COPY) # NOTE: no firebird_writes, no rtc switch: the Agent has none
|
||||
|
||||
def run_job(mode, approved=(), rollbacks=(), job_id=1):
|
||||
out, products, _ = coverage(env, company, ncm_valid)
|
||||
job = {"job_id": job_id, "authorizations": {"rtc_write_mode": mode, "rtc_approved_codes": list(approved), "rtc_rollbacks": list(rollbacks)}}
|
||||
return service._rtc_auto_apply(env, config, job, products, out, company), out
|
||||
|
||||
_, _, cov_before = coverage(env, company, ncm_valid)
|
||||
report["coverage_before"] = cov_before
|
||||
report["resolution_before"] = {k: v for k, v in out["context"]["rtc_resolution"].items() if k != "by_reason"}
|
||||
products = firebird_reader.read_products(env)
|
||||
config = SimpleNamespace(firebird_writes=True, rtc_auto_apply_enabled=True, rtc_auto_apply_max_per_job=500, database_path=COPY)
|
||||
job = {"job_id": 424242, "authorizations": {"rtc_auto_apply": True}}
|
||||
# ---- 7 (first, on purpose): stale decision - RTC switched off on the copy between decision and write => nothing is written
|
||||
rw = connect(True); c = rw.cursor(); c.execute("UPDATE PARAMETROS SET USAR_REFORMA_TRIBUTARIA = 0"); rw.commit(); rw.close()
|
||||
stale = service._rtc_auto_apply(env, config, job, products, out, company)
|
||||
report["stale_decision_run"] = {"write_attempts": stale["write_attempts"], "write_successes": stale["write_successes"], "aborted_reasons": stale["aborted_reasons"]}
|
||||
rw = connect(True); c = rw.cursor(); c.execute("UPDATE PARAMETROS SET USAR_REFORMA_TRIBUTARIA = 1"); rw.commit(); rw.close()
|
||||
# ---- 3. apply
|
||||
first = service._rtc_auto_apply(env, config, job, products, out, company)
|
||||
report["first_run"] = {"mode": first["mode"], "ready_total": first["ready_total"], "write_attempts": first["write_attempts"], "write_successes": first["write_successes"], "aborted_reasons": first["aborted_reasons"],
|
||||
"audit_sample": first["applied"][:3]}
|
||||
# ---- 4/5. re-read, FK, whole-table diff
|
||||
# ---- 2. DISABLED / DRY_RUN / stale decision: zero writes
|
||||
disabled, _ = run_job("DISABLED")
|
||||
dry, out0 = run_job("DRY_RUN")
|
||||
report["disabled_run"] = {"write_attempts": disabled["write_attempts"], "preview": len(disabled["preview"]), "ready_total": disabled["ready_total"]}
|
||||
report["dry_run"] = {"write_attempts": dry["write_attempts"], "preview": len(dry["preview"]), "ready_total": dry["ready_total"], "rules": sorted({c["rule_id"] for c in dry["preview"]}), "sample": dry["preview"][:3]}
|
||||
report["resolution_before"] = {k: v for k, v in out0["context"]["rtc_resolution"].items() if k != "by_reason"}
|
||||
assert base == baseline()[0], "DISABLED/DRY_RUN wrote something"
|
||||
rw = connect(True); c = rw.cursor(); c.execute("UPDATE PARAMETROS SET USAR_REFORMA_TRIBUTARIA = 0"); rw.commit()
|
||||
stale, _ = run_job("ENABLED")
|
||||
c.execute("UPDATE PARAMETROS SET USAR_REFORMA_TRIBUTARIA = 1"); rw.commit(); rw.close()
|
||||
report["stale_decision_run"] = {"write_attempts": stale["write_attempts"], "write_successes": stale["write_successes"]}
|
||||
# ---- 3. APPROVAL_REQUIRED (5 approved) then ENABLED
|
||||
preview_codes = [c["product_code"] for c in dry["preview"]]
|
||||
approved = preview_codes[:5]
|
||||
unapproved = run_job("APPROVAL_REQUIRED", approved=[])[0]
|
||||
part, _ = run_job("APPROVAL_REQUIRED", approved=approved, job_id=2)
|
||||
report["approval_run"] = {"without_approval_attempts": unapproved["write_attempts"], "approved": len(approved), "write_successes": part["write_successes"], "only_approved": sorted(a["product_code"] for a in part["applied"]) == sorted(approved)}
|
||||
applied = list(part["applied"])
|
||||
jobs = 0
|
||||
while True:
|
||||
jobs += 1
|
||||
step, _ = run_job("ENABLED", job_id=100 + jobs)
|
||||
applied += step["applied"]
|
||||
report.setdefault("enabled_jobs", []).append({"write_attempts": step["write_attempts"], "write_successes": step["write_successes"], "aborted": step["aborted_reasons"]})
|
||||
if not step["write_attempts"] or jobs >= 200:
|
||||
break
|
||||
report["total_applied"] = len(applied)
|
||||
report["audit_sample"] = applied[:2]
|
||||
report["audit_fields_complete"] = all(a.get(k) is not None for a in applied for k in ("rule_id", "rule_version", "company_crt", "authorization_origin", "description_snapshot", "timestamp", "job_id", "local_catalog_hash", "official_source_version"))
|
||||
# ---- 4. reread + integrity + coverage
|
||||
mid, mid_values = baseline()
|
||||
report["rows_changed"] = sum(1 for k, v in mid_values.items() if base_values.get(k) != v)
|
||||
ro = connect(False); cur = ro.cursor()
|
||||
cols, after_c25 = snapshot_table(cur, "C000025", "CODIGO")
|
||||
changed = {code: [c for c in cols if before_c25[code][c] != row[c]] for code, row in after_c25.items() if any(before_c25[code][c] != row[c] for c in cols)}
|
||||
report["rows_changed"] = len(changed)
|
||||
report["columns_changed"] = sorted({c for v in changed.values() for c in v})
|
||||
cur.execute("SELECT COUNT(*) FROM C000025 p WHERE p.CCLASSTRIB IS NOT NULL AND NOT EXISTS (SELECT 1 FROM RTC_CCLASSTRIB c WHERE c.CCLASSTRIB = p.CCLASSTRIB)"); report["fk_orphans"] = cur.fetchone()[0]
|
||||
cur.execute("SELECT COUNT(*) FROM C000025 p JOIN RTC_CCLASSTRIB c ON c.CCLASSTRIB = p.CCLASSTRIB WHERE NOT EXISTS (SELECT 1 FROM RTC_CST s WHERE s.CSTIBSCBS = c.CSTIBSCBS)"); report["cst_orphans"] = cur.fetchone()[0]
|
||||
cur.execute("SELECT CCLASSTRIB, COUNT(*) FROM C000025 WHERE CCLASSTRIB IS NOT NULL GROUP BY 1"); report["written_by_class"] = {r[0].strip(): r[1] for r in cur.fetchall()}
|
||||
cur.execute("SELECT COUNT(*) FROM RTC_CCLASSTRIB"); report["catalog_rows_unchanged"] = cur.fetchone()[0]
|
||||
ro.close()
|
||||
out2, cov_after = coverage(env, company, ncm_valid)
|
||||
_, _, cov_after = coverage(env, company, ncm_valid)
|
||||
report["coverage_after"] = cov_after
|
||||
report["resolution_after"] = {k: v for k, v in out2["context"]["rtc_resolution"].items() if k != "by_reason"}
|
||||
# ---- 6. second run
|
||||
products2 = firebird_reader.read_products(env)
|
||||
second = service._rtc_auto_apply(env, config, job, products2, out2, company)
|
||||
report["second_run"] = {"ready_total": second["ready_total"], "write_attempts": second["write_attempts"], "write_successes": second["write_successes"]}
|
||||
report["IDEMPOTENT"] = second["write_attempts"] == 0 and second["write_successes"] == 0
|
||||
report["ONLY_CCLASSTRIB_CHANGED"] = report["columns_changed"] in ([], ["CCLASSTRIB"])
|
||||
report["PASS"] = bool(report["IDEMPOTENT"] and report["ONLY_CCLASSTRIB_CHANGED"] and report["fk_orphans"] == 0 and report["cst_orphans"] == 0 and first["write_successes"] == first["write_attempts"] > 0
|
||||
and report["stale_decision_run"]["write_successes"] == 0 and report["rows_changed"] == first["write_successes"])
|
||||
# ---- 5. second run
|
||||
second, _ = run_job("ENABLED", job_id=300)
|
||||
report["second_run"] = {"write_attempts": second["write_attempts"], "write_successes": second["write_successes"], "ready_total": second["ready_total"]}
|
||||
# ---- 6. rollback of 3 records + 1 conflict
|
||||
print(json.dumps({k: report[k] for k in ("disabled_run","dry_run","stale_decision_run","approval_run","enabled_jobs","coverage_before","resolution_before")}, default=str)[:2500]) if len(applied) < 4 else None
|
||||
victims = applied[:4]
|
||||
rw = connect(True); c = rw.cursor(); c.execute("UPDATE C000025 SET CCLASSTRIB = '200035' WHERE CODIGO = ?", (victims[3]["product_code"],)); rw.commit(); rw.close() # an operator changed it afterwards
|
||||
requests = [{"audit_id": 900 + i, "product_code": v["product_code"], "written_value": v["new_value"], "rule_id": v["rule_id"]} for i, v in enumerate(victims)]
|
||||
roll, _ = run_job("DISABLED", rollbacks=requests, job_id=400) # rollback needs the central request, not a write mode
|
||||
outcomes = {r["product_code"]: (r["outcome"], r.get("reason")) for r in roll["rollbacks"]}
|
||||
report["rollback"] = {"rolled_back": sum(1 for o, _ in outcomes.values() if o == "ROLLED_BACK"), "conflict": outcomes[victims[3]["product_code"]], "records": [outcomes[v["product_code"]][0] for v in victims]}
|
||||
ro = connect(False); cur = ro.cursor()
|
||||
cur.execute("SELECT CODIGO, CCLASSTRIB FROM C000025 WHERE CODIGO IN (%s)" % ",".join("?" * 4), tuple(v["product_code"] for v in victims))
|
||||
after_roll = {str(r[0]).strip(): (r[1] or "").strip() for r in cur.fetchall()}
|
||||
ro.close()
|
||||
report["rollback"]["reread"] = {k: v for k, v in after_roll.items()}
|
||||
# ---- 7. reapply + idempotency
|
||||
again, _ = run_job("ENABLED", job_id=500)
|
||||
report["reapply"] = {"write_successes": again["write_successes"], "codes": sorted(a["product_code"] for a in again["applied"]), "expected": sorted(v["product_code"] for v in victims[:3])}
|
||||
third, _ = run_job("ENABLED", job_id=501)
|
||||
report["idempotency_after_reapply"] = {"write_attempts": third["write_attempts"], "write_successes": third["write_successes"]}
|
||||
# ---- 8. invariants
|
||||
final, final_values = baseline()
|
||||
changed_tables = diff_tables(base, final)
|
||||
report["NON_TARGET_COLUMNS_CHANGED"] = 1 if "C000025" in changed_tables else 0
|
||||
report["CATALOG_CHANGED"] = len([t for t in changed_tables if t.startswith("RTC_")])
|
||||
report["PARAMETERS_CHANGED"] = len([t for t in changed_tables if t == "PARAMETROS"])
|
||||
report["DOCUMENT_TABLES_CHANGED"] = len([t for t in changed_tables if any(h in t for h in DOC_TABLE_HINTS)])
|
||||
report["OTHER_TABLES_CHANGED"] = len(changed_tables)
|
||||
report["changed_tables"] = changed_tables
|
||||
report["IDEMPOTENT"] = second["write_attempts"] == 0 and third["write_attempts"] == 0
|
||||
report["PASS"] = bool(report["IDEMPOTENT"] and report["fk_orphans"] == 0 and report["cst_orphans"] == 0 and not changed_tables and report["total_applied"] > 5
|
||||
and report["disabled_run"]["write_attempts"] == 0 and report["dry_run"]["write_attempts"] == 0 and report["dry_run"]["rules"] == ["RTC-R3"]
|
||||
and report["stale_decision_run"]["write_successes"] == 0 and report["approval_run"]["without_approval_attempts"] == 0 and report["approval_run"]["only_approved"]
|
||||
and report["rows_changed"] == report["total_applied"] and report["rollback"]["rolled_back"] == 3 and report["rollback"]["conflict"][0] == "ABORTED"
|
||||
and report["rollback"]["reread"][victims[3]["product_code"]] == "200035" and all(report["rollback"]["reread"][v["product_code"]] == "" for v in victims[:3])
|
||||
and report["reapply"]["codes"] == report["reapply"]["expected"] and report["audit_fields_complete"])
|
||||
(ROOT / "docs" / "runs" / "rtc_copy_write_e2e.json").write_text(json.dumps(report, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
|
||||
print(json.dumps({k: report[k] for k in ("copy_original_crt", "copy_rtc_param", "coverage_before", "coverage_after", "first_run", "stale_decision_run", "rows_changed", "columns_changed", "fk_orphans", "cst_orphans", "second_run", "IDEMPOTENT", "ONLY_CCLASSTRIB_CHANGED", "PASS")},
|
||||
ensure_ascii=False, default=str)[:3000])
|
||||
print(json.dumps({k: report[k] for k in report if k not in ("audit_sample", "coverage_before", "resolution_before")}, ensure_ascii=False, default=str)[:3500])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
"""SCALE test of the Hub side for a ~21k-product installation (synthetic payloads shaped like the real MMMODAS run).
|
||||
|
||||
python scripts/scale_hub_test.py [--products 21000] [--out docs/runs/scale_hub_test.json]
|
||||
|
||||
Measures: job-result payload size, Hub persistence (blockers upsert + RTC preview + audits) with SQL statement counts (N+1 check),
|
||||
and the panel pages that read the data (dashboard, agent detail, RTC control, blockers). In-process SQLite: timings are a conservative
|
||||
upper bound for the PostgreSQL production database only for CPU-bound work; statement counts are engine-independent.
|
||||
"""
|
||||
import argparse
|
||||
import logging
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from sqlalchemy import event, select # noqa: E402
|
||||
from sqlalchemy.orm import Session # noqa: E402
|
||||
|
||||
from central.tests import test_zero_touch_hub as zt # noqa: E402
|
||||
|
||||
logging.disable(logging.CRITICAL) # the Hub logs each job result; at 21k products that is megabytes of noise
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--products", type=int, default=21000)
|
||||
parser.add_argument("--out", default=str(ROOT / "docs" / "runs" / "scale_hub_test.json"))
|
||||
args = parser.parse_args()
|
||||
n = args.products
|
||||
client, engine = zt._client()
|
||||
counter = {"n": 0}
|
||||
event.listen(engine, "before_cursor_execute", lambda *a, **k: counter.__setitem__("n", counter["n"] + 1))
|
||||
zt.hb(client)
|
||||
with Session(engine) as s:
|
||||
from central.app.models import Installation
|
||||
s.scalars(select(Installation)).one().rtc_write_mode = "APPROVAL_REQUIRED"
|
||||
s.commit()
|
||||
blocked = [{"company": "", "product_code": f"{i:06d}", "gtin": None, "description": f"PRODUTO DE TESTE {i}", "blocker_type": "BLOCKED_FISCAL_DATA", "field": "RTC", "reason_code": "CCLASSTRIB_MISSING_UNRESOLVED",
|
||||
"evidence_summary": {"reason": "RTC-R4:SUGGESTION_ONLY:000001", "human": "cClassTrib sem regra determinística"}, "input_fingerprint": f"fp{i:08d}"} for i in range(n)]
|
||||
preview = [{"product_code": f"{i:06d}", "description": f"PRODUTO DE TESTE {i}", "ncm": "10063021", "current": "", "proposed": "200003", "cst": "200", "rule_id": "RTC-R3", "basis": "RTC-R3:ANNEX"} for i in range(5000)]
|
||||
operational = {**zt.OPERATIONAL, "TOTAL_PRODUCTS": n, "COMPLETE": 0, "SAFE_BLOCKED": n, "BLOCKED_MISSING_EVIDENCE": n}
|
||||
summary = {"operational": operational, "products_processed": n, "products_changed": n, "blocked_products": blocked, "resolved_products": [],
|
||||
"fiscal_context": {"rtc_enabled": True, "evaluated_all": True, "rtc_resolution": {"total": n, "valid_existing": 0, "ready_to_apply": 5000, "ambiguous": 100, "catalog_incompatible": 0}},
|
||||
"rtc_auto_apply": {"mode": "APPROVAL_REQUIRED", "ready_total": 5000, "preview": preview, "preview_truncated": False, "applied": [], "aborted": [], "rollbacks": []}}
|
||||
payload = json.dumps({"result_summary": summary})
|
||||
report = {"products": n, "result_payload_bytes": len(payload), "result_payload_mb": round(len(payload) / 1048576, 2)}
|
||||
job = client.get("/v1/jobs/next", headers=zt.H).json()
|
||||
counter["n"] = 0
|
||||
t0 = time.perf_counter()
|
||||
response = client.post(f"/v1/jobs/{job['job_id']}/complete", content=payload, headers={**zt.H, "content-type": "application/json"})
|
||||
report["persist_first_pass"] = {"status": response.status_code, "seconds": round(time.perf_counter() - t0, 2), "sql_statements": counter["n"], "statements_per_product": round(counter["n"] / n, 2)}
|
||||
# a second full evaluation with the same content (steady state: every 30 min per installation)
|
||||
zt.age_last_sync(engine, 4000); zt.hb(client)
|
||||
job = client.get("/v1/jobs/next", headers=zt.H).json()
|
||||
counter["n"] = 0
|
||||
t0 = time.perf_counter()
|
||||
response = client.post(f"/v1/jobs/{job['job_id']}/complete", content=payload, headers={**zt.H, "content-type": "application/json"})
|
||||
report["persist_steady_state"] = {"status": response.status_code, "seconds": round(time.perf_counter() - t0, 2), "sql_statements": counter["n"], "statements_per_product": round(counter["n"] / n, 2)}
|
||||
with Session(engine) as s:
|
||||
from central.app.models import BlockedProduct, RtcCandidate
|
||||
report["rows"] = {"blocked": len(s.scalars(select(BlockedProduct.id)).all()), "rtc_candidates": len(s.scalars(select(RtcCandidate.id)).all())}
|
||||
# panel pages (authenticated operator session)
|
||||
from central.tests.test_control_panel_pages import _panel_client
|
||||
pc, agent_id, _ = _panel_client()
|
||||
import central.tests.test_control_panel_pages as cp
|
||||
cp._seed_rtc(pc, agent_id, n=5000)
|
||||
from central.app.blocked_products import upsert_blocker
|
||||
from central.app.models import Installation as Inst
|
||||
with pc._session_factory() as s:
|
||||
inst = s.scalar(select(Inst).where(Inst.agent_id == agent_id))
|
||||
for b in blocked:
|
||||
upsert_blocker(s, inst.tenant_id, b, installation_id=inst.id)
|
||||
s.commit()
|
||||
pages = {}
|
||||
for name, url in (("dashboard", "/"), ("agent_detail", f"/agents/{agent_id}"), ("rtc_control_p1", f"/agents/{agent_id}/rtc"), ("rtc_control_p50", f"/agents/{agent_id}/rtc?page=50"), ("blockers", "/blockers")):
|
||||
counter["n"] = 0
|
||||
t0 = time.perf_counter()
|
||||
r = pc.get(url)
|
||||
pages[name] = {"status": r.status_code, "seconds": round(time.perf_counter() - t0, 3), "html_kb": round(len(r.content) / 1024, 1)}
|
||||
report["panel_pages"] = pages
|
||||
Path(args.out).write_text(json.dumps(report, indent=1), encoding="utf-8")
|
||||
print(json.dumps(report))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user