From 2158a30179e0aa282964e49915dda5080ad38990 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 7 Oct 2026 13:46:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=A4=8D=E7=9B=98=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E4=B8=8E=E7=BB=9F=E8=AE=A1=E5=B9=B6=E8=A1=8C=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E6=92=9E=E9=94=81=E5=AF=BC=E8=87=B4=E6=A8=A1=E6=8B=9F?= =?UTF-8?q?=E7=9B=98=E5=8A=A0=E8=BD=BD=E5=A4=B1=E8=B4=A5=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- lib/common/static/options_review.js | 5 +- lib/options/options_review_lib.py | 46 +++++++++++++------ lib/options/options_review_register.py | 38 ++++++++++++--- lib/options/options_time_lib.py | 19 ++++++-- .../templates/options_review_panel.html | 2 +- 5 files changed, 83 insertions(+), 27 deletions(-) diff --git a/lib/common/static/options_review.js b/lib/common/static/options_review.js index 4dd38a6..02a2e5d 100644 --- a/lib/common/static/options_review.js +++ b/lib/common/static/options_review.js @@ -929,7 +929,10 @@ var kpi = $("or-kpi"); var groups = $("or-stats-groups"); if (!kpi || !groups) return; - fetch("/api/options/review/stats?" + baseQs().toString(), { credentials: "same-origin" }) + // 与 trades?sync=1 并行时不再二次同步,避免 SQLite database is locked + var qs = baseQs(); + qs.set("sync", "0"); + fetch("/api/options/review/stats?" + qs.toString(), { credentials: "same-origin" }) .then(function (r) { return r.json(); }) diff --git a/lib/options/options_review_lib.py b/lib/options/options_review_lib.py index 1d9197d..b6ea588 100644 --- a/lib/options/options_review_lib.py +++ b/lib/options/options_review_lib.py @@ -66,21 +66,28 @@ def _safe_float(v: Any) -> Optional[float]: def get_sync_state(conn: sqlite3.Connection, key: str) -> Optional[str]: - row = conn.execute( - "SELECT value FROM options_review_sync_state WHERE key=?", (key,) - ).fetchone() + try: + row = conn.execute( + "SELECT value FROM options_review_sync_state WHERE key=?", (key,) + ).fetchone() + except sqlite3.OperationalError: + return None return str(row["value"]) if row and row["value"] is not None else None def set_sync_state(conn: sqlite3.Connection, key: str, value: str) -> None: - conn.execute( - """ - INSERT INTO options_review_sync_state(key, value, updated_at) - VALUES (?, ?, ?) - ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at - """, - (key, value, _now_str()), - ) + try: + conn.execute( + """ + INSERT INTO options_review_sync_state(key, value, updated_at) + VALUES (?, ?, ?) + ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at + """, + (key, value, _now_str()), + ) + except sqlite3.OperationalError: + # 列表/统计并发刷新时可能撞锁;标记失败不阻断读列表 + pass def _purge_review_trade_by_key(conn: sqlite3.Connection, history_key: str) -> bool: @@ -627,8 +634,16 @@ def ensure_local_review_synced( repair_swapped_closed_at_signal_note, ) - ts_migrate = migrate_options_utc_wall_clock_to_app(conn) - note_swap = repair_swapped_closed_at_signal_note(conn) + ts_migrate: dict[str, Any] = {} + note_swap: dict[str, Any] = {} + try: + ts_migrate = migrate_options_utc_wall_clock_to_app(conn) + except Exception as e: + ts_migrate = {"ok": False, "msg": str(e)} + try: + note_swap = repair_swapped_closed_at_signal_note(conn) + except Exception as e: + note_swap = {"ok": False, "msg": str(e)} if backfill_exchange_pnl and ex is not None: try: from lib.exchange.okx_options_lib import fetch_all_option_positions_history @@ -640,7 +655,10 @@ def ensure_local_review_synced( backfill_closed_options_realized_pnl_from_history(conn, hist) except Exception: pass - out = sync_all_review_sources(conn, ex=ex, from_exchange=False) + try: + out = sync_all_review_sources(conn, ex=ex, from_exchange=False) + except sqlite3.OperationalError as e: + out = {"ok": False, "msg": f"sync locked: {e}"} if isinstance(out, dict): out["ts_migrate"] = ts_migrate out["note_swap"] = note_swap diff --git a/lib/options/options_review_register.py b/lib/options/options_review_register.py index 7b33d45..cae60d9 100644 --- a/lib/options/options_review_register.py +++ b/lib/options/options_review_register.py @@ -291,9 +291,18 @@ def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: s "no", ) if do_sync: - ex, _err = _require_ex(cfg) - ensure_local_review_synced(conn, ex=ex if ex is not None else None) - conn.commit() + try: + ex, _err = _require_ex(cfg) + ensure_local_review_synced(conn, ex=ex if ex is not None else None) + conn.commit() + except Exception as sync_err: + # 同步撞锁时仍返回已有列表,避免整页 500 + print(f"[options_review_trades] sync skipped: {sync_err}", flush=True) + try: + conn.rollback() + except Exception: + pass + init_options_review_tables(conn) filt = dict( source_type=_review_source_for_mode(request.args.get("source_type")), underlying=(request.args.get("underlying") or "").strip() or None, @@ -434,9 +443,26 @@ def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: s def api_options_review_stats(): conn = cfg["get_db"]() try: - ex, _err = _require_ex(cfg) - ensure_local_review_synced(conn, ex=ex if ex is not None else None) - conn.commit() + # 默认不同步:与 trades?sync=1 并行时避免双写撞锁;需要时再 sync=1 + do_sync = (request.args.get("sync") or "0").strip().lower() in ( + "1", + "true", + "yes", + ) + if do_sync: + try: + ex, _err = _require_ex(cfg) + ensure_local_review_synced(conn, ex=ex if ex is not None else None) + conn.commit() + except Exception as sync_err: + print(f"[options_review_stats] sync skipped: {sync_err}", flush=True) + try: + conn.rollback() + except Exception: + pass + init_options_review_tables(conn) + else: + init_options_review_tables(conn) stats = compute_review_stats( conn, source_type=_review_source_for_mode(request.args.get("source_type")), diff --git a/lib/options/options_time_lib.py b/lib/options/options_time_lib.py index 39af29f..ff0c043 100644 --- a/lib/options/options_time_lib.py +++ b/lib/options/options_time_lib.py @@ -112,11 +112,16 @@ def migrate_options_utc_wall_clock_to_app(conn: sqlite3.Connection) -> dict[str, init_options_tables(conn) trades = _shift_table("options_trades", ("created_at", "closed_at")) + except sqlite3.OperationalError: + return {"skipped": 0, "trades": 0, "review": 0, "hidden": 0, "locked": 1} except Exception: trades = 0 - review = _shift_table("options_review_trades", ("opened_at", "closed_at")) - hidden = _shift_table("options_review_hidden", ("closed_at",)) + try: + review = _shift_table("options_review_trades", ("opened_at", "closed_at")) + hidden = _shift_table("options_review_hidden", ("closed_at",)) + except sqlite3.OperationalError: + return {"skipped": 0, "trades": trades, "review": 0, "hidden": 0, "locked": 1} set_sync_state(conn, _MIGRATE_FLAG, "1") return {"skipped": 0, "trades": trades, "review": review, "hidden": hidden} @@ -129,7 +134,7 @@ def repair_swapped_closed_at_signal_note(conn: sqlite3.Connection) -> dict[str, 幂等:同步状态 options_closed_at_note_swap_done=1 后仍扫描可修行(新坏行可再修). """ from lib.options.options_review_db import init_options_review_tables - from lib.options.options_review_lib import set_sync_state + from lib.options.options_review_lib import get_sync_state, set_sync_state init_options_review_tables(conn) trades_fixed = 0 @@ -174,9 +179,13 @@ def repair_swapped_closed_at_signal_note(conn: sqlite3.Connection) -> dict[str, (note, hk, closed), ) review_fixed += int(cur.rowcount or 0) + except sqlite3.OperationalError: + # 并发刷新撞锁时跳过本轮修复,不影响列表读取 + return {"trades": 0, "review": 0, "locked": 1} except Exception: pass - # 复盘表里仍残留非时间戳 closed_at 的,若能从 local_opt 源修好则已处理;否则仅标记完成 - set_sync_state(conn, _SWAP_REPAIR_FLAG, "1") + # 已标记过则不再写同步状态,减少并发写冲突 + if get_sync_state(conn, _SWAP_REPAIR_FLAG) != "1": + set_sync_state(conn, _SWAP_REPAIR_FLAG, "1") return {"trades": trades_fixed, "review": review_fixed} diff --git a/lib/options/templates/options_review_panel.html b/lib/options/templates/options_review_panel.html index 4ef768f..76011c3 100644 --- a/lib/options/templates/options_review_panel.html +++ b/lib/options/templates/options_review_panel.html @@ -456,4 +456,4 @@ - +