Skip to content

Commit 73cffb5

Browse files
committed
fix: avoid blocking account sync lock contention
1 parent ddc3135 commit 73cffb5

2 files changed

Lines changed: 39 additions & 6 deletions

File tree

‎backend_api_python/app/services/live_trading/account_positions.py‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ def sync_account_positions(
3131
exchange_id: str,
3232
market_type: str,
3333
legs: List[AccountLegSnapshot],
34-
) -> None:
34+
) -> bool:
3535
"""
3636
Replace L1 rows for (credential_id, market_type) with the latest exchange snapshot.
3737
"""
3838
cred = int(credential_id or 0)
3939
if cred <= 0:
40-
return
40+
return False
4141
mt = str(market_type or "swap").strip().lower()
4242
if mt in ("futures", "future", "perp", "perpetual"):
4343
mt = "swap"
@@ -48,9 +48,18 @@ def sync_account_positions(
4848
with get_db_connection() as db:
4949
cur = db.cursor()
5050
cur.execute(
51-
"SELECT pg_advisory_xact_lock(hashtextextended(%s, 0))",
51+
"SELECT pg_try_advisory_xact_lock(hashtextextended(%s, 0)) AS acquired",
5252
(f"account-position-sync:{uid}:{cred}:{mt}",),
5353
)
54+
lock_row = cur.fetchone() or {}
55+
acquired = bool(
56+
lock_row.get("acquired")
57+
if isinstance(lock_row, dict)
58+
else lock_row[0]
59+
)
60+
if not acquired:
61+
cur.close()
62+
return False
5463
for leg in legs or []:
5564
sym = normalize_strategy_symbol(leg.symbol) or str(leg.symbol or "").strip()
5665
side = str(leg.side or "").strip().lower()
@@ -118,6 +127,7 @@ def sync_account_positions(
118127
)
119128
db.commit()
120129
cur.close()
130+
return True
121131

122132

123133
def account_legs_from_exchange_maps(

‎backend_api_python/tests/test_position_ownership.py‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@
1919

2020

2121
class _RecordingCursor:
22-
def __init__(self):
22+
def __init__(self, *, lock_acquired=True):
2323
self.calls = []
2424
self.rowcount = 1
25+
self.lock_acquired = lock_acquired
2526

2627
def execute(self, sql, params=None):
2728
self.calls.append((sql, params))
2829

30+
def fetchone(self):
31+
return {"acquired": self.lock_acquired}
32+
2933
def close(self):
3034
return None
3135

@@ -58,20 +62,39 @@ def test_account_position_sync_serializes_each_credential_bucket(monkeypatch):
5862
db = _RecordingDb(cursor)
5963
monkeypatch.setattr(account_positions, "get_db_connection", lambda: _RecordingContext(db))
6064

61-
account_positions.sync_account_positions(
65+
synced = account_positions.sync_account_positions(
6266
user_id=7,
6367
credential_id=33,
6468
exchange_id="gate",
6569
market_type="swap",
6670
legs=[],
6771
)
6872

69-
assert "pg_advisory_xact_lock" in cursor.calls[0][0]
73+
assert synced is True
74+
assert "pg_try_advisory_xact_lock" in cursor.calls[0][0]
7075
assert cursor.calls[0][1] == ("account-position-sync:7:33:swap",)
7176
assert "DELETE FROM qd_account_positions" in cursor.calls[1][0]
7277
assert db.committed is True
7378

7479

80+
def test_account_position_sync_skips_instead_of_waiting_for_busy_bucket(monkeypatch):
81+
cursor = _RecordingCursor(lock_acquired=False)
82+
db = _RecordingDb(cursor)
83+
monkeypatch.setattr(account_positions, "get_db_connection", lambda: _RecordingContext(db))
84+
85+
synced = account_positions.sync_account_positions(
86+
user_id=7,
87+
credential_id=33,
88+
exchange_id="gate",
89+
market_type="swap",
90+
legs=[],
91+
)
92+
93+
assert synced is False
94+
assert len(cursor.calls) == 1
95+
assert db.committed is False
96+
97+
7598
def test_fresh_snapshot_clears_only_older_recorded_drift(monkeypatch):
7699
cursor = _RecordingCursor()
77100
db = _RecordingDb(cursor)

0 commit comments

Comments
 (0)