meow (#1360)
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
"""Diagnose why a tournament bracket is "stuck" on the website.
|
||||
|
||||
The website reads tss_tournaments.db live, so a bracket that never updates means
|
||||
the *scanner* stopped writing fresh rows for that tournament. This script proves
|
||||
where the freeze is by comparing, for each tournament:
|
||||
|
||||
- what is STORED in tss_tournaments.db (and how stale it is),
|
||||
- whether needs_scan() will ever re-scan it (and why not),
|
||||
- what a FRESH live scan from the TSS API would produce right now,
|
||||
- the diff (new battles / changed match statuses the DB is missing).
|
||||
|
||||
If the live scan has battles/matches the DB lacks AND needs_scan() is False, the
|
||||
freeze is in the scanner (a prematurely-stored "finished" status). If the live
|
||||
scan matches the DB, the data source is current and the staleness is downstream
|
||||
(web cache / frontend not refreshing).
|
||||
|
||||
Usage:
|
||||
# Auto-detect frozen tournaments: games arrived after the last scan.
|
||||
python TSSBOT/scripts/diagnose_tournament_freeze.py
|
||||
|
||||
# Diagnose specific tournament ids.
|
||||
python TSSBOT/scripts/diagnose_tournament_freeze.py 25003 25034
|
||||
|
||||
# Tune how many auto-detected tournaments to deep-scan (default 5).
|
||||
python TSSBOT/scripts/diagnose_tournament_freeze.py --top 10
|
||||
"""
|
||||
import argparse
|
||||
import asyncio
|
||||
import pathlib
|
||||
import sqlite3
|
||||
import sys
|
||||
import time
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT.parent / "SHARED"))
|
||||
|
||||
# Match start_bot.py/tss_ws.py: load TSSBOT/.env before importing storage, which
|
||||
# resolves STORAGE_VOL_PATH at import time.
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(dotenv_path=ROOT / ".env")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from BOT.storage import TSS_BATTLES_DB_PATH # noqa: E402
|
||||
from BOT.tss_tournaments import ( # noqa: E402
|
||||
TSS_TOURNAMENTS_DB_PATH,
|
||||
RESCAN_INTERVAL_SECONDS,
|
||||
RESCAN_END_BUFFER_SECONDS,
|
||||
build_scan_sync,
|
||||
needs_scan,
|
||||
)
|
||||
|
||||
|
||||
def _age(unix: Optional[int], now: int) -> str:
|
||||
if not unix:
|
||||
return "never"
|
||||
secs = max(0, now - int(unix))
|
||||
if secs < 90:
|
||||
return f"{secs}s ago"
|
||||
if secs < 5400:
|
||||
return f"{secs // 60}m ago"
|
||||
if secs < 172800:
|
||||
return f"{secs // 3600}h ago"
|
||||
return f"{secs // 86400}d ago"
|
||||
|
||||
|
||||
def stored_state(tid: int) -> Optional[Dict]:
|
||||
"""Stored tournaments row + child-row counts, or None if never scanned."""
|
||||
if not TSS_TOURNAMENTS_DB_PATH.exists():
|
||||
return None
|
||||
with sqlite3.connect(f"file:{TSS_TOURNAMENTS_DB_PATH}?mode=ro", uri=True) as conn:
|
||||
row = conn.execute(
|
||||
"SELECT name, status, date_end, scanned_unix, match_count "
|
||||
"FROM tournaments WHERE tournament_id = ?",
|
||||
(tid,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
status_counts = dict(
|
||||
conn.execute(
|
||||
"SELECT status, COUNT(*) FROM tournament_matches "
|
||||
"WHERE tournament_id = ? GROUP BY status",
|
||||
(tid,),
|
||||
).fetchall()
|
||||
)
|
||||
battle_count = conn.execute(
|
||||
"SELECT COUNT(*) FROM tournament_battles WHERE tournament_id = ?",
|
||||
(tid,),
|
||||
).fetchone()[0]
|
||||
battle_hexes = {
|
||||
r[0]
|
||||
for r in conn.execute(
|
||||
"SELECT session_hex FROM tournament_battles WHERE tournament_id = ?",
|
||||
(tid,),
|
||||
).fetchall()
|
||||
}
|
||||
return {
|
||||
"name": row[0],
|
||||
"status": row[1],
|
||||
"date_end": row[2],
|
||||
"scanned_unix": row[3],
|
||||
"match_count": row[4],
|
||||
"status_counts": status_counts,
|
||||
"battle_count": battle_count,
|
||||
"battle_hexes": battle_hexes,
|
||||
}
|
||||
|
||||
|
||||
def newest_game_unix(tid: int) -> Optional[int]:
|
||||
"""endtime_unix of the most recent game we ingested for this tournament."""
|
||||
with sqlite3.connect(f"file:{TSS_BATTLES_DB_PATH}?mode=ro", uri=True) as conn:
|
||||
row = conn.execute(
|
||||
"SELECT MAX(endtime_unix) FROM match_summary WHERE tournament_id = ?",
|
||||
(tid,),
|
||||
).fetchone()
|
||||
return int(row[0]) if row and row[0] else None
|
||||
|
||||
|
||||
def frozen_candidates() -> List[Tuple[int, Optional[str], int, Optional[int]]]:
|
||||
"""Tournaments with a game ingested AFTER their last scan — the freeze tell.
|
||||
|
||||
Returns (tid, name, newest_game_unix, scanned_unix) sorted by newest game.
|
||||
"""
|
||||
games = {}
|
||||
names = {}
|
||||
with sqlite3.connect(f"file:{TSS_BATTLES_DB_PATH}?mode=ro", uri=True) as conn:
|
||||
for tid, newest, name in conn.execute(
|
||||
"SELECT tournament_id, MAX(endtime_unix), NULLIF(MAX(tournament_name), '') "
|
||||
"FROM match_summary WHERE tournament_id IS NOT NULL AND tournament_id > 0 "
|
||||
"GROUP BY tournament_id"
|
||||
).fetchall():
|
||||
games[int(tid)] = int(newest) if newest else 0
|
||||
names[int(tid)] = name
|
||||
|
||||
scanned: Dict[int, int] = {}
|
||||
if TSS_TOURNAMENTS_DB_PATH.exists():
|
||||
with sqlite3.connect(f"file:{TSS_TOURNAMENTS_DB_PATH}?mode=ro", uri=True) as conn:
|
||||
for tid, sunix in conn.execute(
|
||||
"SELECT tournament_id, scanned_unix FROM tournaments"
|
||||
).fetchall():
|
||||
scanned[int(tid)] = int(sunix) if sunix else 0
|
||||
|
||||
out = []
|
||||
for tid, newest in games.items():
|
||||
sunix = scanned.get(tid, 0)
|
||||
# A game landed after the last scan → the bracket on the site is stale.
|
||||
if newest and newest > sunix:
|
||||
out.append((tid, names.get(tid), newest, scanned.get(tid)))
|
||||
out.sort(key=lambda r: r[2], reverse=True)
|
||||
return out
|
||||
|
||||
|
||||
async def diagnose(tid: int, name: Optional[str], now: int) -> None:
|
||||
print(f"\n{'=' * 72}\nTournament {tid}" + (f" ({name})" if name else ""))
|
||||
print("=" * 72)
|
||||
|
||||
stored = stored_state(tid)
|
||||
newest_game = newest_game_unix(tid)
|
||||
|
||||
if stored is None:
|
||||
print(" STORED: never scanned (no row in tss_tournaments.db)")
|
||||
else:
|
||||
print(f" STORED: status={stored['status']!r} "
|
||||
f"last scan {_age(stored['scanned_unix'], now)} "
|
||||
f"matches={stored['match_count']} battles={stored['battle_count']}")
|
||||
print(f" match statuses: {stored['status_counts']}")
|
||||
print(f" date_end {_age(stored['date_end'], now)}")
|
||||
print(f" NEWEST INGESTED GAME: {_age(newest_game, now)}")
|
||||
if stored and newest_game and stored["scanned_unix"]:
|
||||
if newest_game > stored["scanned_unix"]:
|
||||
print(" >> A game arrived AFTER the last scan — the stored bracket is stale.")
|
||||
|
||||
will_rescan = await needs_scan(tid, now=now)
|
||||
reason = ""
|
||||
if stored is None:
|
||||
reason = "never scanned"
|
||||
elif stored["status"] == "finished":
|
||||
reason = "status=='finished' → needs_scan() is permanently False (FROZEN)"
|
||||
elif stored["date_end"] and now > stored["date_end"] + RESCAN_END_BUFFER_SECONDS:
|
||||
reason = "past date_end + buffer → re-scan disabled"
|
||||
else:
|
||||
nxt = (stored["scanned_unix"] or 0) + RESCAN_INTERVAL_SECONDS
|
||||
reason = f"interval-gated; next eligible {_age(nxt, now)} ({'due' if nxt <= now else 'waiting'})"
|
||||
print(f" needs_scan(): {will_rescan} — {reason}")
|
||||
|
||||
print(" LIVE SCAN (fetching from TSS API, not stored)…", flush=True)
|
||||
try:
|
||||
scan = build_scan_sync(tid, fallback_name=name, now=now)
|
||||
except Exception as exc:
|
||||
print(f" LIVE SCAN FAILED: {exc}")
|
||||
return
|
||||
|
||||
live_hexes = {b["session_hex"] for b in scan["battles"]}
|
||||
live_status_counts: Dict[str, int] = {}
|
||||
for m in scan["matches"]:
|
||||
live_status_counts[m["status"]] = live_status_counts.get(m["status"], 0) + 1
|
||||
print(f" LIVE: status={scan['status']!r} matches={scan['match_count']} "
|
||||
f"battles={len(scan['battles'])}")
|
||||
print(f" match statuses: {live_status_counts}")
|
||||
|
||||
stored_hexes = stored["battle_hexes"] if stored else set()
|
||||
new_battles = live_hexes - stored_hexes
|
||||
print(f" DIFF: live has {len(new_battles)} battle(s) the DB is missing; "
|
||||
f"match_count {stored['match_count'] if stored else 0} → {scan['match_count']}")
|
||||
|
||||
if new_battles and not will_rescan:
|
||||
print(" VERDICT: FROZEN SCANNER — live data is newer but needs_scan() is "
|
||||
"False, so the bracket will never update. Root cause is the scanner.")
|
||||
elif new_battles and will_rescan:
|
||||
print(" VERDICT: due to re-scan — data source is fine; staleness is timing "
|
||||
"or downstream (web cache / frontend not refreshing).")
|
||||
elif not new_battles:
|
||||
print(" VERDICT: DB already current with live — if the site still looks "
|
||||
"stale, the freeze is downstream (web cache / frontend polling).")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("tournament_ids", nargs="*", type=int,
|
||||
help="tournament ids to diagnose (default: auto-detect frozen ones)")
|
||||
parser.add_argument("--top", type=int, default=5,
|
||||
help="how many auto-detected tournaments to deep-scan (default 5)")
|
||||
args = parser.parse_args()
|
||||
now = int(time.time())
|
||||
|
||||
print(f"battles db: {TSS_BATTLES_DB_PATH}")
|
||||
print(f"tournament db: {TSS_TOURNAMENTS_DB_PATH}")
|
||||
|
||||
if args.tournament_ids:
|
||||
targets = [(tid, None) for tid in args.tournament_ids]
|
||||
else:
|
||||
candidates = frozen_candidates()
|
||||
print(f"\nAuto-detected {len(candidates)} tournament(s) with a game ingested "
|
||||
f"after their last scan (stale brackets):")
|
||||
for tid, name, newest, sunix in candidates[: args.top * 4]:
|
||||
print(f" {tid:>8} game {_age(newest, now):>10} "
|
||||
f"scan {_age(sunix, now):>10} {name or ''}")
|
||||
if not candidates:
|
||||
print(" (none — no tournament has games newer than its last scan)")
|
||||
return
|
||||
print(f"\nDeep-scanning the top {min(args.top, len(candidates))}…")
|
||||
targets = [(tid, name) for tid, name, _, _ in candidates[: args.top]]
|
||||
|
||||
for tid, name in targets:
|
||||
await diagnose(tid, name, now)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user