ServerSocket: bypass download bandwidth throttler for control traffic (#393) - #615
Merged
mrjimenez merged 1 commit intoMay 15, 2026
Merged
Conversation
CEMSocket::OnReceive() gates every read through CDownloadBandwidthThrottler. CServerSocket inherits from CEMSocket and so shared that path, which caused server-control traffic -- search results, OP_FoundSources, OP_ServerMessage, MOTD -- to stall when the throttler's bucket ran out. Reproducer: set Preferences -> Connection -> Max Download to a tight cap (e.g. 40 KB/s) while peer downloads consume the budget. A search lands on the server, the response arrives as TCP bytes in the kernel, but CEMSocket::OnReceive() sees Reserve() return 0, sets pendingOnReceive, and returns. The server socket sits with bytes buffered and no Packet Received log line fires; the GUI shows the search as having produced no results. WakeIfPaused() rearms when budget refills, but for a small bucket and a 20 KB compressed search payload the per-tick allowance is too small to make progress before the user gives up. Lifting the cap makes the symptom disappear. Control traffic should not be on the data-plane budget in the first place. Search/source/MOTD packets are tiny, latency-sensitive, and have no user benefit from being throttled. Add a virtual IsDownloadThrottled() on CEMSocket defaulting to true (peer file-transfer sockets); CServerSocket overrides to false. The receive path captures the flag once at the start of each iteration and skips Reserve()/Refund() entirely when false. Behaviour for CClientTCPSocket and the rest of the file-transfer path is unchanged -- the throttler still gates them, with the same Reserve/Refund bookkeeping. Reported by @mifritscher2 in amule-project#393.
This was referenced May 15, 2026
Closed
mrjimenez
pushed a commit
that referenced
this pull request
May 15, 2026
PR #615 added IsDownloadThrottled() with the override keyword on CServerSocket. That made Clang's -Winconsistent-missing-override fire on the existing CServerSocket overrides (OnClose / OnConnect / OnReceive / OnError / PacketReceived / SendPacket) that were missing the keyword. Reported by Stoatwblr on #622. Add override to the affected six methods in CServerSocket, and apply the same cleanup to CClientTCPSocket -- which has the identical inheritance shape from CEMSocket and was one stray override keyword away from triggering the same warning. Also drop the redundant virtual keyword on the derived overrides (override implies virtual). Build clean on macOS arm64; warning gone on the Linux build path Stoatwblr was using.
mrjimenez
pushed a commit
to mrjimenez/amule
that referenced
this pull request
Jul 27, 2026
…nnect (amule-project#620) Adding a burst of downloads to a large queue over the remote GUI froze the GUI for seconds and dripped the new files in at only a few per second (issue amule-project#615). Selecting ~100 search results for download on a ~10k queue was the reporter's repro. CKnownFilesRem::ProcessUpdate() only wrapped the list ctrls in BeginBatchUpdate()/EndBatchUpdate() for the post-reconnect reconcile (issue amule-project#444). On an ordinary steady-state poll the batch was never engaged, so each freshly-added partfile went through CDownloadListCtrl::AddFile() with the per-item SortList() active: a full re-sort of the entire list on every insert. For a 10k queue that is O(n^2 log n) and blocks the GUI event loop, which in turn throttles the outbound download requests -- hence the ~4/sec drip the reporter saw. Batch the download list on every non-initial poll: BeginBatchUpdate() suppresses the per-item sort, and the single SortList() runs once at the end, and only when the poll actually added a file (downloadListGrew). A pure in-place stat poll stays sort-free, so the common case pays nothing. EndBatchUpdate() gains a doSort parameter (default true) to express that. The cold-boot m_initialUpdate path keeps its own ShowFileList() batching and is left untouched. The shared-files ctrl batching is unchanged (reconnect-only), matching its existing behaviour. Refs amule-project#615
mrjimenez
pushed a commit
to mrjimenez/amule
that referenced
this pull request
Jul 27, 2026
amule-project#621) Downloading a large multi-selection of search results into a big queue froze the monolithic GUI, the same way amule-project#615 did over the remote GUI -- just on a different, un-batched path. CSearchListCtrl::DownloadSelected() loops over the selection calling Search_Add_Download per file. In the monolithic build that notification runs synchronously on the main thread, so each file's AddFile() fires a per-item SortList() inline -- a full re-sort of the whole download list on every insert, O(n^2) on a large queue. Wrap the selection loop in the download list's BeginBatchUpdate() / EndBatchUpdate() so the burst collapses into a single sort + repaint, mirroring the remote GUI's poll path (amule-project#620). Monolithic-only: the remote GUI's adds arrive later via the download-queue poll, which already batches, so the code is gated behind #ifndef CLIENT_GUI.
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.
References #393.
Bug
With a tight
Preferences -> Connection -> Max Downloadcap (40 KB/s in the report), peer downloads consume the throttler budget and the server socket stalls silently. Searches return no results in the GUI even though wireshark shows the server sending the response packets. Source discovery viaOP_FoundSourcesis affected the same way — sources stop arriving while KAD keeps working.Root cause
CEMSocket::OnReceive()atsrc/EMSocket.cpp:220-247gates every read throughCDownloadBandwidthThrottler::Get().Reserve(readMax).CServerSocket : public CEMSocketinherits the same receive path — so server-control traffic (search results,OP_FoundSources,OP_ServerMessage, MOTD) competes for the same byte budget as peer file-transfer reads.When the budget is exhausted,
Reserve()returns 0,OnReceive()setspendingOnReceiveand returns. The bytes sit in the kernel buffer; noPacket Receivedlog line fires, no parser runs, the GUI sees nothing.WakeIfPaused()rearms on the next throttler tick, but for a small bucket and a 20 KB compressed search payload the per-tick allowance is too small to make practical progress before the user gives up. Lifting the download cap makes the symptom disappear — confirmed by @mifritscher2.Server-control traffic is tiny, latency-sensitive, and has no user benefit from being throttled. Putting it on the data-plane budget at all was the design error.
Fix
Add a virtual
IsDownloadThrottled() constonCEMSocket, defaulting totrue(peer file-transfer sockets, i.e.CClientTCPSocket).CServerSocketoverrides tofalse.OnReceivecaptures the flag once at the start of each iteration and skips theReserve/Refundcalls entirely when false; theRead()proceeds with the fullreadMax. Behaviour for peer sockets is unchanged — same Reserve/Refund bookkeeping, same wake path, samependingOnReceivesemantics.Risk surface
CClientTCPSocket(peer file transfer) — behaviour unchanged. Same throttler reservation, same refund-on-error/refund-on-block, samependingOnReceiveandWakeIfPaused()flow.CServerSocket(server control) — skips throttler entirely. ReadsreadMaxbytes directly. Volume is tiny (search responses, source replies, server status), so unthrottled reads can't realistically saturate anyone's downlink.Reported by @mifritscher2 in #393 — they pinpointed the offending block by reading the source after the wireshark capture ruled out parser-side issues.