fix(linux): bound L2CAP connect timeout - #25
Conversation
|
Thanks — the ~40-second event-loop stall is valid. I reproduced the same class of blocking locally with Linux VHCI/ PR #22 merged after this branch was created, so this now conflicts with The combined behavior should be implemented on top of the current nonblocking
That preserves your important case where the kernel L2CAP path never finishes and raw HCI is required, while retaining #22's protection when the controller already completed and failed an actual attempt. I suggest rebasing onto current Maintainer edits are enabled, so I can also adapt the branch after the rebase if useful. |
fb06e67 to
9ad6a81
Compare
|
Maintainer integration update:
Linux VHCI validation with two BlueZ virtual controllers:
The Linux ARM64 native addon also compiles cleanly, and lint passes. The repository test command retains its pre-existing caught TypeError on macOS while exiting successfully; this patch does not affect that harness. |
9ad6a81 to
ebddbad
Compare
|
Agreed on all counts — Thanks for reproducing it on VHCI and for doing the rework. Nothing needed from me, happy for you to carry the branch. |
BluetoothHciL2Socket::connect() runs on the thread driving libuv. A peer that does not complete the kernel L2CAP connection can otherwise block the event loop for roughly 40 seconds. Use a two-second poll deadline by default. Allow BLUETOOTH_HCI_L2CAP_CONNECT_TIMEOUT_MS to override it, and let 0 restore the unbounded behavior. A local deadline closes the pending kernel socket before raw HCI fallback, while an explicit controller failure still suppresses a duplicate attempt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ebddbad to
fe20530
Compare
|
Rebased onto main after #26 merged. The updated head includes @semantic-release/github 12.0.9, and all 14 required validation/test/build checks pass across Linux, macOS, Windows, Android, ARM, and x64. Marking ready for review. |
stoprocent
left a comment
There was a problem hiding this comment.
Validated the timeout state transitions locally and with BlueZ VHCI, then revalidated the rebased head across the complete GitHub Actions matrix. Approved for merge.
|
🎉 This PR is included in version 2.2.8 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Bound the L2CAP connect that blocks the event loop for ~40 s per BLE connect
BluetoothHciL2Socket::connect()calls::connect()with no send timeout, on thethread running libuv. When the peer doesn't complete that L2CAP connection it
blocks for the kernel's full L2CAP timeout — ~40 s measured — and nothing else in
the process runs meanwhile: no timers, no I/O, no callbacks.
This adds a bounded wait, default 2 s,
BLUETOOTH_HCI_L2CAP_CONNECT_TIMEOUT_MSto override,
0for current behaviour. Nothing else changes: on failure thesocket is still closed and
isConnected()still returns false, so the callerstill forwards the original
LE_CREATE_CONN— which is what establishes the linkin this case anyway.
Why it's hard to spot
It doesn't look like Bluetooth. MQTT connects time out, unrelated timers fire
late — I measured a 20 s
setTimeoutfiring at 61 s — and CPU stays idlethroughout, so it doesn't look like a busy loop either.
Evidence
Acaia Pearl scale, Raspberry Pi 4 / BCM43455, kernel 6.18.34, BlueZ 5.82, via
@abandonware/bluetooth-hci-socket0.5.3-12 (same code).Main thread during the stall, from
/proc— inconnect(2), every other threadidle:
An event-loop lag probe across a connect, before and after:
One alternative I tried, because it's the obvious one
Keeping the socket open on timeout — so
isConnected()stays true and noduplicate
LE_CREATE_CONNgoes out — never connects at all. Thisconnect()is the attempt; when it can't complete, closing and falling through is the only
path that works. Also rejected:
HCI_CHANNEL_USER=1connects in 24–53 ms butGATT never completes.
Scope and caveats
2 s is ~100× headroom for peripherals where this succeeds (milliseconds on an
established ACL link) while bounding the damage where it doesn't. The defect is
general — an unbounded blocking wait on the loop thread — but I've only seen one
peripheral trigger it, so I can't speak to how common that is.
Patch is against
mainand compiles cleanly (node-gyp rebuild, arm64 Debian13). The runtime numbers come from the identical code in the
@abandonwarepackage, which is what my deployment runs — so treat the build as verified here
and the behaviour as inherited rather than retested on this branch.
Happy to change the default, the env var name, or move the constant to a header.