Skip to content

ServerSocket: bypass download bandwidth throttler for control traffic (#393) - #615

Merged
mrjimenez merged 1 commit into
amule-project:masterfrom
got3nks:fix/server-socket-no-throttle
May 15, 2026
Merged

ServerSocket: bypass download bandwidth throttler for control traffic (#393)#615
mrjimenez merged 1 commit into
amule-project:masterfrom
got3nks:fix/server-socket-no-throttle

Conversation

@got3nks

@got3nks got3nks commented May 15, 2026

Copy link
Copy Markdown
Contributor

References #393.

Bug

With a tight Preferences -> Connection -> Max Download cap (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 via OP_FoundSources is affected the same way — sources stop arriving while KAD keeps working.

Root cause

CEMSocket::OnReceive() at src/EMSocket.cpp:220-247 gates every read through CDownloadBandwidthThrottler::Get().Reserve(readMax). CServerSocket : public CEMSocket inherits 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() sets pendingOnReceive and returns. The bytes sit in the kernel buffer; no Packet Received log 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() const on CEMSocket, defaulting to true (peer file-transfer sockets, i.e. CClientTCPSocket). CServerSocket overrides to false. OnReceive captures the flag once at the start of each iteration and skips the Reserve/Refund calls entirely when false; the Read() proceeds with the full readMax. Behaviour for peer sockets is unchanged — same Reserve/Refund bookkeeping, same wake path, same pendingOnReceive semantics.

Risk surface

  • CClientTCPSocket (peer file transfer) — behaviour unchanged. Same throttler reservation, same refund-on-error/refund-on-block, same pendingOnReceive and WakeIfPaused() flow.
  • CServerSocket (server control) — skips throttler entirely. Reads readMax bytes directly. Volume is tiny (search responses, source replies, server status), so unthrottled reads can't realistically saturate anyone's downlink.
  • Throttler accounting — server traffic is no longer counted against the user's download cap. Users who set a strict cap will see slightly more bytes/second than the cap allows during search bursts; in practice the difference is in the hundreds-of-bytes range and was already invisible.

Reported by @mifritscher2 in #393 — they pinpointed the offending block by reading the source after the wireshark capture ruled out parser-side issues.

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.
@mrjimenez
mrjimenez merged commit 356a59c into amule-project:master May 15, 2026
12 checks passed
@got3nks
got3nks deleted the fix/server-socket-no-throttle branch May 15, 2026 15:56
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants