Skip to content

Commit

Permalink
better support for 700+ connections
Browse files Browse the repository at this point in the history
when there was more than ~700 active connections,
* sendfile (non-https downloads) could fail
* mdns and ssdp could fail to reinitialize on network changes

...because `select` can't handle FDs higher than 512 on windows
(1024 on linux/macos), so prefer `poll` where possible (linux/macos)

but apple keeps breaking and unbreaking `poll` in macos,
so use `--no-poll` if necessary to force `select` instead
  • Loading branch information
9001 committed May 31, 2024
1 parent ac1bc23 commit 07b2bf1
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 7 deletions.
8 changes: 7 additions & 1 deletion copyparty/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import locale
import os
import re
import select
import socket
import sys
import threading
Expand Down Expand Up @@ -1335,6 +1336,8 @@ def add_debug(ap):
ap2 = ap.add_argument_group('debug options')
ap2.add_argument("--vc", action="store_true", help="verbose config file parser (explain config)")
ap2.add_argument("--cgen", action="store_true", help="generate config file from current config (best-effort; probably buggy)")
if hasattr(select, "poll"):
ap2.add_argument("--no-poll", action="store_true", help="kernel-bug workaround: disable poll; use select instead (limits max num clients to ~700)")
ap2.add_argument("--no-sendfile", action="store_true", help="kernel-bug workaround: disable sendfile; do a safe and slow read-send-loop instead")
ap2.add_argument("--no-scandir", action="store_true", help="kernel-bug workaround: disable scandir; do a listdir + stat on each file instead")
ap2.add_argument("--no-fastboot", action="store_true", help="wait for initial filesystem indexing before accepting client requests")
Expand Down Expand Up @@ -1545,7 +1548,7 @@ def main(argv: Optional[list[str]] = None, rsrc: Optional[str] = None) -> None:
if hard > 0: # -1 == infinite
nc = min(nc, int(hard / 4))
except:
nc = 512
nc = 486 # mdns/ssdp restart headroom; select() maxfd is 512 on windows

retry = False
for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]:
Expand Down Expand Up @@ -1638,6 +1641,9 @@ def main(argv: Optional[list[str]] = None, rsrc: Optional[str] = None) -> None:
if not hasattr(os, "sendfile"):
al.no_sendfile = True

if not hasattr(select, "poll"):
al.no_poll = True

# signal.signal(signal.SIGINT, sighandler)

SvcHub(al, dal, argv, "".join(printed)).run()
Expand Down
9 changes: 8 additions & 1 deletion copyparty/httpcli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3193,7 +3193,14 @@ def tx_file(self, req_path: str, ptop: Optional[str] = None) -> bool:

sendfun = sendfile_kern if use_sendfile else sendfile_py
remains = sendfun(
self.log, lower, upper, f, self.s, self.args.s_wr_sz, self.args.s_wr_slp
self.log,
lower,
upper,
f,
self.s,
self.args.s_wr_sz,
self.args.s_wr_slp,
not self.args.no_poll,
)

if remains > 0:
Expand Down
25 changes: 23 additions & 2 deletions copyparty/mdns.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,22 @@ def run(self) -> None:
def run2(self) -> None:
last_hop = time.time()
ihop = self.args.mc_hop

try:
if self.args.no_poll:
raise Exception()
fd2sck = {}
srvpoll = select.poll()
for sck in self.srv:
fd = sck.fileno()
fd2sck[fd] = sck
srvpoll.register(fd, select.POLLIN)
except Exception as ex:
srvpoll = None
if not self.args.no_poll:
t = "WARNING: failed to poll(), will use select() instead: %r"
self.log(t % (ex,), 3)

while self.running:
timeout = (
0.02 + random.random() * 0.07
Expand All @@ -300,8 +316,13 @@ def run2(self) -> None:
if self.unsolicited
else (last_hop + ihop if ihop else 180)
)
rdy = select.select(self.srv, [], [], timeout)
rx: list[socket.socket] = rdy[0] # type: ignore
if srvpoll:
pr = srvpoll.poll(timeout * 1000)
rx = [fd2sck[x[0]] for x in pr if x[1] & select.POLLIN]
else:
rdy = select.select(self.srv, [], [], timeout)
rx: list[socket.socket] = rdy[0] # type: ignore

self.rx4.cln()
self.rx6.cln()
buf = b""
Expand Down
24 changes: 22 additions & 2 deletions copyparty/ssdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,29 @@ def run(self) -> None:
self.log("stopped", 2)

def run2(self) -> None:
try:
if self.args.no_poll:
raise Exception()
fd2sck = {}
srvpoll = select.poll()
for sck in self.srv:
fd = sck.fileno()
fd2sck[fd] = sck
srvpoll.register(fd, select.POLLIN)
except Exception as ex:
srvpoll = None
if not self.args.no_poll:
t = "WARNING: failed to poll(), will use select() instead: %r"
self.log(t % (ex,), 3)

while self.running:
rdy = select.select(self.srv, [], [], self.args.z_chk or 180)
rx: list[socket.socket] = rdy[0] # type: ignore
if srvpoll:
pr = srvpoll.poll((self.args.z_chk or 180) * 1000)
rx = [fd2sck[x[0]] for x in pr if x[1] & select.POLLIN]
else:
rdy = select.select(self.srv, [], [], self.args.z_chk or 180)
rx: list[socket.socket] = rdy[0] # type: ignore

self.rxc.cln()
buf = b""
addr = ("0", 0)
Expand Down
12 changes: 11 additions & 1 deletion copyparty/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2517,6 +2517,7 @@ def sendfile_py(
s: socket.socket,
bufsz: int,
slp: int,
use_poll: bool,
) -> int:
remains = upper - lower
f.seek(lower)
Expand Down Expand Up @@ -2545,22 +2546,31 @@ def sendfile_kern(
s: socket.socket,
bufsz: int,
slp: int,
use_poll: bool,
) -> int:
out_fd = s.fileno()
in_fd = f.fileno()
ofs = lower
stuck = 0.0
if use_poll:
poll = select.poll()
poll.register(out_fd, select.POLLOUT)

while ofs < upper:
stuck = stuck or time.time()
try:
req = min(2 ** 30, upper - ofs)
select.select([], [out_fd], [], 10)
if use_poll:
poll.poll(10000)
else:
select.select([], [out_fd], [], 10)
n = os.sendfile(out_fd, in_fd, ofs, req)
stuck = 0
except OSError as ex:
# client stopped reading; do another select
d = time.time() - stuck
if d < 3600 and ex.errno == errno.EWOULDBLOCK:
time.sleep(0.02)
continue

n = 0
Expand Down

0 comments on commit 07b2bf1

Please sign in to comment.