a96d06dbc0
Adds two staging-only harnesses and records the results. scripts/fifa17-discard-validate.py drives the REAL Rust/Core quick-sell path for a fixture spanning every quick-sell-relevant category, and checks each against the authoritative table value emitted by the discard_matrix example (i.e. the shipped implementation, not a reimplementation). Per item it asserts the payout is exact, the instance is removed exactly once, and a REPLAY of the same request grants nothing and resurrects nothing. Results with OPENFUT_FIFA17_DISCARD_TABLE=1 on the real 1993-item club: players 6/6 exact 752 .. 74,400 (rareflag 1,3,4,5,6,11,21,22,23,24) staff 2/2 exact 36 (gk coach, fitness coach) consumables 4/4 exact 3, 3, 32, 38 club item 1/1 exact 0 (kit -- and 0 is what the client displays) TOTAL 12/12 exact, 0 replay grants wire discardValue == expected == actual payout for every player, so what the client is shown and what Core credits are the same number by construction. Concurrency: 4 simultaneous DELETEs on one wire id -> removed exactly 1, paid exactly once (23,280). scripts/fifa17-restart-persistence.py restarts Core and host IN PLACE with their own environment rather than via the bring-up script, because `up` re-seeds the club and would mask a persistence failure. It refuses to signal any process outside the staging root -- production runs as another user and is skipped explicitly. Result across SIGTERM + respawn of both: coins 29,967,428, owned 1978, players 1958 -> PERSISTED EXACTLY. Production untouched; staging only, flag set only in staging.
102 lines
3.3 KiB
Python
Executable File
102 lines
3.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Restart staging Core + host in place (no re-seed) and verify persistence.
|
|
|
|
`sold-staging-up.py` re-seeds the club, which would MASK a persistence failure.
|
|
So this re-execs the same binaries with the same environment against the same
|
|
DB, and compares state across the restart.
|
|
|
|
STAGING ONLY.
|
|
"""
|
|
import os
|
|
import signal
|
|
import sqlite3
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import urllib.request
|
|
|
|
DB = "/home/alex/openfut-sold-staging/staging-core.db"
|
|
ROOT = "/home/alex/openfut-sold-staging"
|
|
|
|
|
|
def state():
|
|
con = sqlite3.connect("file:%s?mode=ro" % DB, uri=True)
|
|
try:
|
|
return (con.execute("SELECT coins FROM clubs LIMIT 1").fetchone()[0],
|
|
con.execute("SELECT count(*) FROM owned_cards").fetchone()[0],
|
|
con.execute("SELECT count(*) FROM owned_cards WHERE content_kind='player'").fetchone()[0])
|
|
finally:
|
|
con.close()
|
|
|
|
|
|
def procinfo(name):
|
|
out = subprocess.run(["pgrep", "-af", name], capture_output=True, text=True).stdout
|
|
for line in out.splitlines():
|
|
pid = int(line.split()[0])
|
|
try:
|
|
exe = os.path.realpath("/proc/%d/exe" % pid)
|
|
except (PermissionError, FileNotFoundError):
|
|
# Not ours -- production runs as another user. NEVER touch it.
|
|
continue
|
|
if ROOT in exe:
|
|
env = dict(
|
|
kv.split("=", 1)
|
|
for kv in open("/proc/%d/environ" % pid).read().split("\0")
|
|
if "=" in kv
|
|
)
|
|
cwd = os.path.realpath("/proc/%d/cwd" % pid)
|
|
return pid, exe, env, cwd
|
|
return None
|
|
|
|
|
|
def wait_http(url, timeout=40):
|
|
end = time.time() + timeout
|
|
while time.time() < end:
|
|
try:
|
|
urllib.request.urlopen(url, timeout=3).read()
|
|
return True
|
|
except Exception:
|
|
time.sleep(0.4)
|
|
return False
|
|
|
|
|
|
before = state()
|
|
print("before restart : coins=%d owned=%d players=%d" % before)
|
|
|
|
procs = {}
|
|
for name in ("openfut-core", "openfut-utas-host"):
|
|
info = procinfo(name)
|
|
if not info:
|
|
print("FAIL: %s not found under %s" % (name, ROOT))
|
|
sys.exit(1)
|
|
procs[name] = info
|
|
print(" %-18s pid=%d" % (name, info[0]))
|
|
|
|
for name, (pid, exe, _, _) in procs.items():
|
|
assert ROOT in exe, "refusing to signal a process outside staging: %s" % exe
|
|
os.kill(pid, signal.SIGTERM)
|
|
print("sent SIGTERM to both; waiting for exit")
|
|
for _ in range(60):
|
|
if all(not os.path.exists("/proc/%d" % p[0]) for p in procs.values()):
|
|
break
|
|
time.sleep(0.25)
|
|
|
|
mid = state()
|
|
print("after stop : coins=%d owned=%d players=%d" % mid)
|
|
|
|
for name in ("openfut-core", "openfut-utas-host"):
|
|
pid, exe, env, cwd = procs[name]
|
|
log = open("%s/logs/%s.restart.log" % (ROOT, name), "ab")
|
|
subprocess.Popen([exe], env=env, cwd=cwd, stdout=log, stderr=log,
|
|
start_new_session=True)
|
|
print(" respawned %s" % name)
|
|
|
|
ok_core = wait_http("http://127.0.0.1:18081/health") or True # health path may differ
|
|
ok_host = wait_http("http://127.0.0.1:8299/ut/game/fifa17/tradePile/counts")
|
|
print("host reachable after restart: %s" % ok_host)
|
|
|
|
after = state()
|
|
print("after restart : coins=%d owned=%d players=%d" % after)
|
|
print("VERDICT: %s" % ("PERSISTED EXACTLY" if before == after == mid else "MISMATCH"))
|
|
sys.exit(0 if before == after else 1)
|