Repository navigation
Sockets: fixes to ClientSocket_WinSocket - #1774
Conversation
59dbf35 to
7a161c8
Compare
7a161c8 to
976dd48
Compare
|
Two updates:
|
|
New commit (607b722) - fixes the intermittent Calling It now does the portable pattern instead: one Expectation for your rapid-reset test: the first reconnect right after a reset may still fail - the dock is probably RSTing the new attempt while it is still tearing down the old connection (the new connect likely reuses the same 4-tuple, since the old one was RST-closed, not FIN-closed). But the status will now show the real reason (reset/refused) instead of 10022, and the retry ~100 ms later succeeds, as in your log. Re-verified with the mock-winsock harness: added a scenario where the connect attempt settles as a failure mid-flight - the implementation reports the real error and never calls connect() a second time (the mock reproduces the Windows WSAEINVAL quirk, so the old loop would have reported 10022 on it). Functional + stress suites all pass. |
The Windows socket implementation was stale and had several bugs: - connect() called ThreadPool::blocking_dispatch(), which no longer exists (the API is dispatch_now_blocking()); the file did not compile. - Connection resets were not detected: when the peer closed the connection, recv() == 0 looped forever at 1 ms, and on socket errors (e.g. WSAECONNRESET) the receive thread exited, but the state stayed CONNECTED and no callback fired. Fire the on_disconnect() callback added in 'Sockets: add Listener::on_disconnect() hook' when the connection is lost, and return the socket to NOT_RUNNING. - close() only set a flag: the socket was never closed and the state was stuck at DESTRUCTING forever, so the object could not be reused. close() now waits for the receive thread to exit (including any in-flight listener callback), closes the socket, and returns the socket to NOT_RUNNING so connect() can be called again. It is safe to call multiple times, and must not be called from within a listener callback. - send() counted and advanced by the requested bytes instead of the bytes actually sent, so a partial send silently dropped the remainder and reported full success. - The connect-failure paths called m_lock.unlock() while a std::unique_lock on the same mutex was still active (double unlock). - WSAEINVAL in the connect loop was retried forever; it is now treated as a fatal error like the other connect errors. A socket whose previous connection has ended cannot be reconnected, so connect() creates a fresh socket each time. Not wired in yet: ClientSocket.h still selects ClientSocket_Qt on Windows.
Nothing in the program ever calls WSAStartup, so the WinSocket backend was relying on Qt having initialized Winsock first. If this backend is used before any Qt networking call, the first socket() fails with WSA_NOT_INITIALIZED (10093). Call WSAStartup exactly once, before the first socket is created. WSACleanup is deliberately never called: it is a process-wide, reference-counted operation and Qt may still be using Winsock.
Calling connect() again while a non-blocking connect is in progress is not safe on Windows: once the attempt fails, the second connect() returns WSAEINVAL (10022) instead of starting a new attempt. This made rapid connect/disconnect cycles fail intermittently with 'WSA Error Code: 10022' - the real failure (usually a reset or refusal while the peer is still tearing down the previous connection) was hidden. Do the portable thing instead of retrying connect(): call connect() once, wait for the socket to become writable with select(), and read the result with getsockopt(SO_ERROR). The real error is now reported (refused -> 'Connection refused.', timeout -> 'Connection timed out.', anything else -> its WSA code).
Now that dispatch() spawns threads as needed, the receive thread no longer needs dispatch_now_blocking(). The blocking variant can also deadlock if connect() is called from a thread that is itself running on a capped pool, since it blocks until a pool thread is free.
14185d2 to
19f116f
Compare
Follow-up to the review of
Common/Cpp/Sockets(the Windows one that isn't used on Windows because "it has issues"). This fixes theClientSocket_WinSocketimplementation so it can be examined/tested.What was broken
It didn't compile.
connect()calledThreadPool::blocking_dispatch(), which no longer exists (the API isdispatch_now_blocking(); seeCommon/Cpp/Concurrency/ThreadPool.h). The file had been stale since the thread-pool API was renamed.Connection resets were never detected. When the peer closed the connection,
recv() == 0entered an infinite 1 ms poll loop; on socket errors (e.g.WSAECONNRESET) the receive thread exited. Either way the state stayedCONNECTEDand no callback fired, so the owner (sysbotbase) believed it was still connected forever.close()didn't close the socket and left the state stuck atDESTRUCTINGpermanently, so the object could never be reused (one-shot).Partial-send data loss.
send()counted/advanced by the requested bytes (current) instead of the bytes actually sent (current_sent), so a partial send silently dropped the remainder and reported full success. (POSIX got this fixed in4d3797d20.)Double mutex unlock. The connect-failure paths called
m_lock.unlock()on the raw mutex while astd::unique_lockon that same mutex was still active (UB / crash on the "connection refused" path).Minor:
WSAEINVALin the connect loop was retried forever; theINVALID_SOCKETguard insend()was dead code becausem_socketwasconst.What the fix does
AbstractClientSocket::Listener::on_disconnect(error_message)callback (empty by default, so it's backward-compatible). Fired when an established connection is lost (server close or socket error). It is not fired byclose().NOT_RUNNING, soconnect()can be called again on the same object.close()now actually tears down: it waits for the receive thread to exit — including any in-flight listener callback — closes the socket, and returns the socket toNOT_RUNNING. This also closes the use-after-free window where the owner could destroy itself while the receive thread was still calling back into it (theremove_listenerfix in Sockets: fix AbstractClientSocket::remove_listener() #1772 was the other half of that). It's idempotent, but must not be called from within a listener callback (it would wait on itself).send()now accounts forcurrent_sent.connect()creates a fresh socket each time, since a socket whose previous connection ended cannot be reconnected. Socket creation failure reports throughon_connect_finished.m_socketis nowstd::atomic<SOCKET>(initializedINVALID_SOCKETin the constructor rather than eagerly creating the socket).The sysbotbase connection overrides
on_disconnect()to log the disconnect and update the status line, so a restart of sys-botbase is now visible instead of a zombie "Last Ack: Ns ago".Not wired in:
Common/Cpp/Sockets/ClientSocket.hstill selectsClientSocket_Qton Windows. Flip the alias there if you want to try this.Testing
Can't build/run the Windows file on Linux, so it was verified with a mock
winsock.hharness driving the real class + thread pool:on_disconnect+NOT_RUNNING, send-after-disconnect returns 0, reconnect on the same object, RST →on_disconnect+NOT_RUNNING, idempotentclose(), connect-refused → error callback with no crash (the old double-unlock path), and retry-after-refused.close()while a slowon_receive_datais in flight blocks until it finishes and no callback runs afterclose()returns; 200 rapid connect/disconnect cycles.The Linux build (
pa-build) and full command-line test suite (pa-test, 20,150 tests) pass with theAbstractClientSocket.hand sysbotbase changes.