Summary
The sync client callback polling thread can spin indefinitely after the underlying Engine.IO socket has stopped.
When RawClient::poll() returns Ok(None), Iter::next() converts that into Some(Err(Error::StoppedEngineIoSocket)). The callback poller then receives that error, but poll_callback only treats IncompleteResponseFromEngineIo and Socket.IO Disconnect packets as reconnect/stop conditions. Other errors currently fall through as no-ops, so the for packet in self_clone.iter() loop immediately polls again.
That creates a tight loop when the Engine.IO socket stays stopped.
Impact
In a long-running sync client process using a realtime Socket.IO feed, this showed up as a background polling thread consuming a significant portion of a small ARMv6 device CPU for multiple days after the feed had stopped. The application itself was otherwise idle and waiting for events.
This is especially noticeable on small or embedded devices, but the same loop can waste CPU anywhere the stopped socket state persists.
Expected behavior
A stopped Engine.IO socket should be handled as a terminal or reconnect condition:
- If reconnect is enabled, attempt reconnect using the existing reconnect path.
- If reconnect is disabled, stop the callback poller instead of continuing to poll immediately.
- Notify the close callback once when polling stops because the underlying Engine.IO socket is closed.
Actual behavior
StoppedEngineIoSocket is ignored by the sync callback poller. The iterator keeps yielding the same error and the polling thread spins without backoff.
Relevant code path
socketio/src/client/client.rs: Iter::next() maps Ok(None) to Error::StoppedEngineIoSocket.
socketio/src/client/client.rs: poll_callback() does not currently handle that error as a stop or reconnect condition.
Proposed fix
I opened a draft PR with a small fix and regression coverage. I will link it below.
Summary
The sync client callback polling thread can spin indefinitely after the underlying Engine.IO socket has stopped.
When
RawClient::poll()returnsOk(None),Iter::next()converts that intoSome(Err(Error::StoppedEngineIoSocket)). The callback poller then receives that error, butpoll_callbackonly treatsIncompleteResponseFromEngineIoand Socket.IODisconnectpackets as reconnect/stop conditions. Other errors currently fall through as no-ops, so thefor packet in self_clone.iter()loop immediately polls again.That creates a tight loop when the Engine.IO socket stays stopped.
Impact
In a long-running sync client process using a realtime Socket.IO feed, this showed up as a background polling thread consuming a significant portion of a small ARMv6 device CPU for multiple days after the feed had stopped. The application itself was otherwise idle and waiting for events.
This is especially noticeable on small or embedded devices, but the same loop can waste CPU anywhere the stopped socket state persists.
Expected behavior
A stopped Engine.IO socket should be handled as a terminal or reconnect condition:
Actual behavior
StoppedEngineIoSocketis ignored by the sync callback poller. The iterator keeps yielding the same error and the polling thread spins without backoff.Relevant code path
socketio/src/client/client.rs:Iter::next()mapsOk(None)toError::StoppedEngineIoSocket.socketio/src/client/client.rs:poll_callback()does not currently handle that error as a stop or reconnect condition.Proposed fix
I opened a draft PR with a small fix and regression coverage. I will link it below.