PINK: fix event-loop corruption in open_positions/open_orders + 23-test orphan suite
Root cause: open_positions()/open_orders() called _backend_snapshot() -> _call_backend() -> _run() -> pool.submit(asyncio.run, coro) which spawned a temporary event loop in a worker thread. httpx AsyncClient created inside that temp loop, loop closed immediately. All subsequent HTTP calls raised Event loop is closed or asyncio.locks.Event bound to different loop. Crash triggered WS stream reconnects; each reconnect re-ran reconcile with N>1 BingX positions and orphaned all but the largest. Fix: open_positions()/open_orders() now read backend._state (populated by await backend.connect() in the main loop). Fallback to _backend_snapshot() for callers without a connected backend. Fixes test_bingx_bugs::TestConnectNoDoubleRefresh: connect() is now async. New test_orphan_prevention.py: 23 tests covering all 5 orphan mechanisms: A. open_positions/open_orders use backend._state, never hit thread pool B. connect() awaitable, backend.connect() runs in main event loop C. Reconcile guard: >1 position logs ERROR and takes only largest D. clientOrderId p-action-base36-rand4 on every order E. EXIT sizing capped to kernel slot_size 391 passed, 2 skipped, 0 failed across all 14 test files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -544,7 +544,9 @@ class TestEventsFromCancelIgnoresSnapshots:
|
||||
class TestConnectNoDoubleRefresh:
|
||||
def test_connect_calls_backend_connect_once_no_extra_snapshot(self):
|
||||
"""connect() must call backend.connect() exactly once and not call
|
||||
refresh_state() a second time (the redundant _backend_snapshot was removed)."""
|
||||
refresh_state() a second time (the redundant _backend_snapshot was removed).
|
||||
connect() is async — must be awaited."""
|
||||
import asyncio
|
||||
backend = MagicMock()
|
||||
backend.connect = AsyncMock(return_value=True)
|
||||
backend.refresh_state = AsyncMock(return_value=_flat_snap())
|
||||
@@ -556,7 +558,7 @@ class TestConnectNoDoubleRefresh:
|
||||
venue._snapshot_ready.set()
|
||||
venue._last_snapshot = None
|
||||
|
||||
result = venue.connect()
|
||||
result = asyncio.run(venue.connect())
|
||||
assert result is True
|
||||
# backend.connect called once
|
||||
backend.connect.assert_called_once()
|
||||
@@ -564,7 +566,9 @@ class TestConnectNoDoubleRefresh:
|
||||
backend.refresh_state.assert_not_called()
|
||||
|
||||
def test_connect_without_backend_connect_is_noop(self):
|
||||
"""If backend has no connect(), connect() returns True without crashing."""
|
||||
"""If backend has no connect(), connect() returns True without crashing.
|
||||
connect() is async — must be awaited."""
|
||||
import asyncio
|
||||
backend = MagicMock(spec=[]) # empty spec — no attributes
|
||||
venue = BingxVenueAdapter.__new__(BingxVenueAdapter)
|
||||
venue.backend = backend
|
||||
@@ -574,7 +578,7 @@ class TestConnectNoDoubleRefresh:
|
||||
venue._snapshot_ready.set()
|
||||
venue._last_snapshot = None
|
||||
|
||||
result = venue.connect()
|
||||
result = asyncio.run(venue.connect())
|
||||
assert result is True
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user