The stress harness found this: 38 completed uploads logged as errors, and a
run that took 30s to move what should take 6.
After the last byte we waited up to 30 seconds for the downloader to close
the file connection. Soulseek does leave that close to the downloader, but a
peer that simply leaves the socket open — or whose FIN a NAT swallows — is
not a failure: it has every byte. We were marking those uploads Failed,
skipping the SendUploadSpeed report that sets our advertised speed, and
holding the upload slot the next peer was queued for.
Cutting the wait to five seconds was not enough, and stress said so: with
two slots and sixty-four queued leechers, five seconds apiece is a hundred
and sixty seconds of waiting, and ten uploads never got served inside the
run. The slot has to be free the moment the file is delivered, so the wait
is gone — Nicotine+ finishes the transfer there too and closes from its own
side. That profile went from 146s with ten uploads lost to 9.7s with none;
the default profile from 30.7s and 25 MiB/s to 5.8s and 133 MiB/s.
Also: a decompression ceiling on peer payloads, the numbers Nicotine+ uses
(128 MiB for a search or folder reply, 2 GiB for a browse listing). Deflate
reaches roughly 1000:1, so a few kilobytes from an unknown peer could
otherwise expand until the process died.
And the stress harness counted threads with `ps -M`, which is macOS
spelling: on Linux it returns one unrelated line, so the guard against
thread explosions has been reporting a single thread on CI all along.
/proc/self/status has the real number.
Reading Nicotine+ and slskd against what was just written turned up three
places where this client had the message right and the behaviour wrong.
Excluded search phrases (code 160) are not a filter on the searches we
send — the server already drops those. Nicotine+ applies them to the files
it offers, leaving out any whose path carries an excluded phrase, and a
reply with nothing left is not sent. Ours now does the same, and the
local refusal of outgoing searches is gone.
Serving children now follows the same rules as Nicotine+: the limit comes
from the server's ParentMinSpeed and ParentSpeedRatio against the upload
speed the server records for us (capped at ten, none at all below the
minimum), children are refused until something feeds us the search stream
and let go when it stops, a second link from a child we already carry is
refused, and AcceptChildren is re-sent whenever that answer changes. Where
the server has never reported our speed there is nothing to apply the
formula to, so the configured cap stands, which is how slskd runs.
Queued downloads now ask where they sit every five minutes, as Nicotine+
does, instead of only when a caller asks; a brokered file connection we
cannot complete reports CantConnectToPeer like a control connection does;
and a peer's shares can be asked for one folder at a time (peer code 36
and its reply, 37), which is what "download folder" is built on.
A browse listed each file as a name and a size, so a searcher who saw a
bitrate in a search reply lost it the moment they opened the sharer's
files. Listings (codes 5 and 37) now carry each file's attributes the
way Nicotine+ sends them, as a SharedFileEntry with name, size and
attributes in place of the (name, size) pair; the CLI's browse record
gains bitrate and duration, and the daemon protocol moves to version 2
for the new listing shape.
A browse or folder listing and a search reply went out as zlib "stored"
blocks, so a big share cost every browser its full uncompressed size,
and the inflater ignored the adler32 trailer, so corrupt data passed.
Listings now deflate through a fixed-Huffman encoder over an LZ77 match
finder, which shrinks a listing's repeated path prefixes to about a
tenth, and a stream whose trailer does not match is refused. No crate:
the encoder is a hundred lines beside the inflater it feeds, and the
inflater is named for what it does.
"Download folder" in SoulseekQt and Nicotine+ asks the sharer for that
one folder (peer code 36) instead of the whole share, and a sharer that
never answers leaves the folder download hanging. The requested folder
now comes back as a FolderContentsResponse (code 37), which carries a
directory listing in the same form a browse does, so the two share one
codec. The browse and folder replies also share one reply helper.