Drift
Python's SidecarWsClient.close() shares self._lock with subscribe()/subscribe_batch(), both of which hold that lock for the entire duration of _ensure_connected() — including its full multi-attempt connect-retry loop (up to CONNECT_ATTEMPTS attempts, each with a 10s socket-connect timeout plus inter-attempt backoff, so potentially 30+ seconds). If a caller invokes close() from another thread while a subscribe()/subscribe_batch() call is stuck retrying a failing connection, close() blocks on with self._lock: until that retry sequence finishes releasing the lock — it cannot promptly interrupt or tear down the client.
TypeScript's close() has no locking at all and always runs to completion immediately; its ensureConnected() retry loop has no equivalent shared-mutex dependency with close().
TypeScript SDK
sdks/typescript/pmxt/ws-client.ts:454-467 — close() is unconditionally synchronous: sets this.closed = true, clears the ping interval, closes the socket, and clears dataQueues/dataStore, with no lock/mutex involved anywhere in the class. Its connect-retry loop lives in ensureConnected() at sdks/typescript/pmxt/ws-client.ts:109-144, which is also lock-free (JS is single-threaded; concurrent subscribe() calls await the same connectPromise, line 114).
Python SDK
sdks/python/pmxt/ws_client.py:393-405 — close():
def close(self) -> None:
"""Close the WebSocket connection."""
self._closed = True
if self._ws:
try:
self._ws.close()
except Exception as e:
import logging
logging.warning("WebSocket close error: %s", e)
self._ws = None
with self._lock:
self._data_queues.clear()
self._data_store.clear()
self._lock is the same lock acquired by subscribe() (sdks/python/pmxt/ws_client.py:311, with self._lock:) and subscribe_batch() (sdks/python/pmxt/ws_client.py:357, with self._lock:) around their call to _ensure_connected() (sdks/python/pmxt/ws_client.py:102-181), whose retry loop runs at lines 151-167 (up to CONNECT_ATTEMPTS iterations, each _connect_websocket(ws, url, timeout=10) at line 154, with time.sleep backoff between attempts). On a fresh/never-connected client self._ws is still None when close() runs, so the if self._ws: branch is skipped entirely and close()'s only remaining work — the trailing with self._lock: block — is exactly the statement that contends with the in-progress retry loop.
Expected
close() should behave the same way in both SDKs: an immediate, best-effort teardown that a caller can rely on to interrupt a client regardless of what other operations are in flight, matching TypeScript's lock-free implementation. Python's shared single threading.Lock couples an unrelated teardown call to the worst-case duration of a concurrent connection attempt.
Impact
A Python caller using close() as a fast/best-effort shutdown or timeout-cancellation mechanism (e.g. during process shutdown, or to abandon a client stuck talking to an unreachable host) can have that call hang for up to the full connect-retry duration instead of returning immediately, which is surprising and not documented. It also means two unrelated subscribe() calls for different exchanges/symbols unnecessarily serialize behind each other's connection attempt via the same lock that close() depends on.
Found by automated SDK cross-language drift audit
Drift
Python's
SidecarWsClient.close()sharesself._lockwithsubscribe()/subscribe_batch(), both of which hold that lock for the entire duration of_ensure_connected()— including its full multi-attempt connect-retry loop (up toCONNECT_ATTEMPTSattempts, each with a 10s socket-connect timeout plus inter-attempt backoff, so potentially 30+ seconds). If a caller invokesclose()from another thread while asubscribe()/subscribe_batch()call is stuck retrying a failing connection,close()blocks onwith self._lock:until that retry sequence finishes releasing the lock — it cannot promptly interrupt or tear down the client.TypeScript's
close()has no locking at all and always runs to completion immediately; itsensureConnected()retry loop has no equivalent shared-mutex dependency withclose().TypeScript SDK
sdks/typescript/pmxt/ws-client.ts:454-467—close()is unconditionally synchronous: setsthis.closed = true, clears the ping interval, closes the socket, and clearsdataQueues/dataStore, with no lock/mutex involved anywhere in the class. Its connect-retry loop lives inensureConnected()atsdks/typescript/pmxt/ws-client.ts:109-144, which is also lock-free (JS is single-threaded; concurrentsubscribe()calls await the sameconnectPromise, line 114).Python SDK
sdks/python/pmxt/ws_client.py:393-405—close():self._lockis the same lock acquired bysubscribe()(sdks/python/pmxt/ws_client.py:311,with self._lock:) andsubscribe_batch()(sdks/python/pmxt/ws_client.py:357,with self._lock:) around their call to_ensure_connected()(sdks/python/pmxt/ws_client.py:102-181), whose retry loop runs at lines 151-167 (up toCONNECT_ATTEMPTSiterations, each_connect_websocket(ws, url, timeout=10)at line 154, withtime.sleepbackoff between attempts). On a fresh/never-connected clientself._wsis stillNonewhenclose()runs, so theif self._ws:branch is skipped entirely andclose()'s only remaining work — the trailingwith self._lock:block — is exactly the statement that contends with the in-progress retry loop.Expected
close()should behave the same way in both SDKs: an immediate, best-effort teardown that a caller can rely on to interrupt a client regardless of what other operations are in flight, matching TypeScript's lock-free implementation. Python's shared singlethreading.Lockcouples an unrelated teardown call to the worst-case duration of a concurrent connection attempt.Impact
A Python caller using
close()as a fast/best-effort shutdown or timeout-cancellation mechanism (e.g. during process shutdown, or to abandon a client stuck talking to an unreachable host) can have that call hang for up to the full connect-retry duration instead of returning immediately, which is surprising and not documented. It also means two unrelatedsubscribe()calls for different exchanges/symbols unnecessarily serialize behind each other's connection attempt via the same lock thatclose()depends on.Found by automated SDK cross-language drift audit