fix: WAL-inclusive backup + reachable scorecard GO + complete run log
Browse files
trading/trading_state_sync.py
CHANGED
|
@@ -102,6 +102,45 @@ def _download_bytes(remote: str) -> bytes | None:
|
|
| 102 |
return None
|
| 103 |
|
| 104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
def upload_trading_state(market_id: str) -> bool:
|
| 106 |
"""Push state.json + trading.db to Supabase. Returns True if anything uploaded."""
|
| 107 |
if not _enabled():
|
|
@@ -120,8 +159,9 @@ def upload_trading_state(market_id: str) -> bool:
|
|
| 120 |
db_path = get_trading_db_path(market_id)
|
| 121 |
if Path(db_path).exists():
|
| 122 |
try:
|
| 123 |
-
|
| 124 |
-
|
|
|
|
| 125 |
ok = True
|
| 126 |
except Exception as e:
|
| 127 |
logger.warning("trading_state: db read/upload error (%s): %s", market_id, e)
|
|
@@ -157,6 +197,13 @@ def download_trading_state(market_id: str, *, overwrite: bool = False) -> bool:
|
|
| 157 |
if body:
|
| 158 |
db_path.parent.mkdir(parents=True, exist_ok=True)
|
| 159 |
db_path.write_bytes(body)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
restored = True
|
| 161 |
|
| 162 |
if restored:
|
|
|
|
| 102 |
return None
|
| 103 |
|
| 104 |
|
| 105 |
+
def _consistent_db_snapshot(db_path: Path) -> bytes | None:
|
| 106 |
+
"""Produce a consistent full snapshot of a SQLite DB, INCLUDING un-checkpointed
|
| 107 |
+
WAL contents.
|
| 108 |
+
|
| 109 |
+
TradingStore runs in WAL mode: recent transactions live in `trading.db-wal`
|
| 110 |
+
until a checkpoint, so a raw file copy of the main .db misses them — in
|
| 111 |
+
production this froze the remote backup days behind the live data. The
|
| 112 |
+
sqlite3 online-backup API reads *through* the WAL, giving a complete,
|
| 113 |
+
transactionally-consistent image even while the worker holds open
|
| 114 |
+
connections.
|
| 115 |
+
"""
|
| 116 |
+
import sqlite3
|
| 117 |
+
import tempfile
|
| 118 |
+
|
| 119 |
+
tmp = tempfile.NamedTemporaryFile(prefix="dbsnap_", suffix=".db", delete=False)
|
| 120 |
+
tmp_path = Path(tmp.name)
|
| 121 |
+
tmp.close()
|
| 122 |
+
try:
|
| 123 |
+
src = sqlite3.connect(str(db_path))
|
| 124 |
+
try:
|
| 125 |
+
dst = sqlite3.connect(str(tmp_path))
|
| 126 |
+
try:
|
| 127 |
+
src.backup(dst)
|
| 128 |
+
dst.commit()
|
| 129 |
+
finally:
|
| 130 |
+
dst.close()
|
| 131 |
+
finally:
|
| 132 |
+
src.close()
|
| 133 |
+
return tmp_path.read_bytes()
|
| 134 |
+
except Exception as e:
|
| 135 |
+
logger.warning("trading_state: db snapshot failed for %s: %s", db_path, e)
|
| 136 |
+
return None
|
| 137 |
+
finally:
|
| 138 |
+
try:
|
| 139 |
+
tmp_path.unlink(missing_ok=True)
|
| 140 |
+
except Exception:
|
| 141 |
+
pass
|
| 142 |
+
|
| 143 |
+
|
| 144 |
def upload_trading_state(market_id: str) -> bool:
|
| 145 |
"""Push state.json + trading.db to Supabase. Returns True if anything uploaded."""
|
| 146 |
if not _enabled():
|
|
|
|
| 159 |
db_path = get_trading_db_path(market_id)
|
| 160 |
if Path(db_path).exists():
|
| 161 |
try:
|
| 162 |
+
payload = _consistent_db_snapshot(Path(db_path))
|
| 163 |
+
if payload and _upload_bytes(_db_remote(market_id), payload,
|
| 164 |
+
"application/octet-stream"):
|
| 165 |
ok = True
|
| 166 |
except Exception as e:
|
| 167 |
logger.warning("trading_state: db read/upload error (%s): %s", market_id, e)
|
|
|
|
| 197 |
if body:
|
| 198 |
db_path.parent.mkdir(parents=True, exist_ok=True)
|
| 199 |
db_path.write_bytes(body)
|
| 200 |
+
# Drop any leftover WAL/SHM siblings: mixing a freshly-restored main
|
| 201 |
+
# file with a stale WAL from a previous incarnation corrupts state.
|
| 202 |
+
for suffix in ("-wal", "-shm"):
|
| 203 |
+
try:
|
| 204 |
+
Path(str(db_path) + suffix).unlink(missing_ok=True)
|
| 205 |
+
except Exception:
|
| 206 |
+
pass
|
| 207 |
restored = True
|
| 208 |
|
| 209 |
if restored:
|