asyncio and native threads timeout handling fix#230
Open
Conversation
Ensure `PubNubAsyncioException` always carries a valid `PNStatus` with error data instead of `None`. fix(asyncio): fix `PubNubAsyncioException.__str__` crash Handle cases where status or `error_data` is `None` instead of raising `AttributeError`. fix(event-engine): fix error type checks in effects Match `PubNubAsyncioException` which is what `request_future` actually returns on failure. fix(event-engine): fix give-up logic for unlimited retries Handle `-1 (unlimited)` correctly since `attempts > -1` was always `true`, causing immediate give-up. fix(event-engine): initialize heartbeat max retry attempts Use delay class defaults instead of config value which could be `None` causing `TypeError` on comparison. fix(event-engine): add missing return after `heartbeat` give-up Prevent falling through to start a heartbeat after deciding to give up. fix(request-handlers): use explicit `httpx.Timeout` object Set all four timeout fields explicitly instead of a 2-tuple that left write and pool unset. fix(request-handlers): enforce wall-clock deadline to survive system sleep On macOS and Linux, `time.monotonic()` does not advance during system sleep, causing socket and `asyncio` timeouts (310s subscribe) to stall for hours of wall-clock time. Add `time.time()`-based deadline checks that detect sleep and cancel stale requests within ~5s of wake. fix(asyncio): replace `asyncio.wait_for` with wall-clock-aware loop Use `asyncio.wait()` with periodic `time.time()` checks instead of a single monotonic-based `wait_for()`, yielding to the event loop between checks. fix(native-threads): add `WallClockDeadlineWatchdog` Persistent single daemon thread monitors `time.time()` every 5s and closes the `httpx` session when the wall-clock deadline passes, interrupting the blocking socket read. Tracks deadlines per calling thread so concurrent requests (e.g., subscribe + publish) don't interfere. Only armed for long-timeout requests (>30s). Session is recreated for subsequent requests test(wall-clock-deadline): add unit tests for sleep detection Cover both `asyncio` and threads paths: simulated clock jumps, normal passthrough, clean watchdog shutdown, per-thread deadline isolation, concurrent request independence, cleanup, and exception propagation. test(native-threads): add try/finally cleanup to subscribe tests Ensure `pubnub.stop()` always runs to prevent non-daemon threads from blocking process exit. test(native-threads): fix flaky where_now and here_now tests Enable presence heartbeat and use unique channel names so presence registers on the server. test(file-upload): fix shared state leak in file upload tests Restore `cipher_key` after use in `send_file` and pass it explicitly to `download_file`. test(message-actions): use unique channel names Avoid collisions with stale data from prior test runs.
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
asyncio and native threads timeout handling fixasyncio and native threads timeout handling fix
…t during sleep Session was never recreated after the wall-clock watchdog closed it, causing all reconnection `/time/0` calls to fail permanently with "client has been closed". fix(native-threads): use socket.shutdown to immediately unblock reads on macOS On macOS, `socket.close()` from another thread does not interrupt a blocked `recv()`. Use `socket.shutdown(SHUT_RDWR)` on the raw TCP socket to unblock within seconds instead of `~25` minutes. fix(reconnect): classify wall-clock sleep timeouts as unexpected disconnect Sleep-induced timeouts were mapped to `PNTimeoutCategory` which triggers an immediate silent restart, bypassing the reconnection manager. Map them to `PNUnexpectedDisconnectCategory` so all paths use the configured retry policy (`exponential`/`linear`). build(deps): pin `httpx<1.0` for internal attribute stability The socket shutdown fix accesses `httpcore` private attributes to reach the raw TCP socket. Pin upper bound to prevent silent breakage on major version changes; access is wrapped in try/except fallback. test(wall-clock-deadline): add tests for sleep/wake reconnection fixes Cover session recreation, `PNERR_CONNECTION_ERROR` mapping, `WallClockTimeoutError` classification, socket shutdown attribute path, and graceful degradation when `httpcore` internals change.
Python 3.13.2's `VERIFY_X509_STRICT` rejects some certifi CA certs, breaking `httpx.Client()` init. Using minor-only versions lets setup-python resolve to latest patches automatically.
jguz-pubnub
approved these changes
Mar 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(asyncio): fix error propagation in async request path
Ensure
PubNubAsyncioExceptionalways carries a validPNStatuswith error data instead ofNone.fix(asyncio): fix
PubNubAsyncioException.__str__crashHandle cases where status or
error_dataisNoneinstead of raisingAttributeError.fix(event-engine): fix error type checks in effects
Match
PubNubAsyncioExceptionwhich is whatrequest_futureactually returns on failure.fix(event-engine): fix give-up logic for unlimited retries
Handle
-1 (unlimited)correctly sinceattempts > -1was alwaystrue, causing immediate give-up.fix(event-engine): initialize heartbeat max retry attempts
Use delay class defaults instead of config value which could be
NonecausingTypeErroron comparison.fix(event-engine): add missing return after
heartbeatgive-upPrevent falling through to start a heartbeat after deciding to give up.
fix(request-handlers): use explicit
httpx.TimeoutobjectSet all four timeout fields explicitly instead of a 2-tuple that left write and pool unset.
fix(request-handlers): enforce wall-clock deadline to survive system sleep
On macOS and Linux,
time.monotonic()does not advance during system sleep, causing socket andasynciotimeouts (310s subscribe) to stall for hours of wall-clock time. Addtime.time()-based deadline checks that detect sleep and cancel stale requests within ~5s of wake.fix(asyncio): replace
asyncio.wait_forwith wall-clock-aware loopUse
asyncio.wait()with periodictime.time()checks instead of a single monotonic-basedwait_for(), yielding to the event loop between checks.fix(native-threads): add
WallClockDeadlineWatchdogPersistent single daemon thread monitors
time.time()every 5s and closes thehttpxsession when the wall-clock deadline passes, interrupting the blocking socket read. Tracks deadlines per calling thread so concurrent requests (e.g., subscribe + publish) don't interfere. Only armed for long-timeout requests (>30s). Session is recreated for subsequent requeststest(wall-clock-deadline): add unit tests for sleep detection
Cover both
asyncioand threads paths: simulated clock jumps, normal passthrough, clean watchdog shutdown, per-thread deadline isolation, concurrent request independence, cleanup, and exception propagation.test(native-threads): add try/finally cleanup to subscribe tests
Ensure
pubnub.stop()always runs to prevent non-daemon threads from blocking process exit.test(native-threads): fix flaky where_now and here_now tests
Enable presence heartbeat and use unique channel names so presence registers on the server.
test(file-upload): fix shared state leak in file upload tests
Restore
cipher_keyafter use insend_fileand pass it explicitly todownload_file.test(message-actions): use unique channel names
Avoid collisions with stale data from prior test runs.