mirror of
https://github.com/3proxy/3proxy.git
synced 2026-10-17 08:55:49 +08:00
A service's TCP listening socket is kept as a listener: when a reload commits, before the old services are told to stop, each new service takes over the listener of an older configuration's service on the same address, port, network namespace and device with the same -ol options. It skips socket() and bind(), calling only listen() again for its own backlog, and logs "Accepting connections, taken over". The old service stops accepting at its next poll and lets go of the socket; whoever lets go last closes it, unlinking a Unix socket's path. So a service staying where it was no longer leaves a gap: before, the old one closed its socket and the new one opened another, and a connection queued on the old socket was reset. UDP services, whose clients read from the service's socket, and two services of the same configuration on one address keep their own sockets as before. A test case makes requests nonstop through three reloads and wants none refused or reset; against the build before this, connections were reset. The reload case no longer waits out the switch. SIGUSR1 in 3proxy(8) describes how a reload behaves now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
2.9 KiB
Python
84 lines
2.9 KiB
Python
"""Listening sockets across a reload: a service of the new configuration on
|
|
the same address and port takes over the old service's socket, so clients
|
|
connecting all the while are never refused nor reset."""
|
|
|
|
import socket
|
|
import threading
|
|
import time
|
|
|
|
|
|
def run(t):
|
|
origin = t.free_port()
|
|
proxy = t.free_port()
|
|
adm = t.free_port()
|
|
conf = f"""
|
|
log
|
|
auth iponly
|
|
allow *
|
|
http data * /data
|
|
httpsrv -p{origin}
|
|
|
|
flush
|
|
auth iponly
|
|
allow *
|
|
proxy -p{proxy}
|
|
admin -p{adm}
|
|
"""
|
|
server = t.start("takeover", conf, ports=[origin, proxy, adm])
|
|
|
|
done = {"ok": 0, "failed": 0, "errors": []}
|
|
stop = threading.Event()
|
|
|
|
def client():
|
|
url = f"http://127.0.0.1:{origin}/data?size=2000"
|
|
while not stop.is_set():
|
|
r = t.http(url, proxy=f"127.0.0.1:{proxy}")
|
|
if r.status == 200 and r.length == 2000:
|
|
done["ok"] += 1
|
|
else:
|
|
done["failed"] += 1
|
|
if len(done["errors"]) < 3:
|
|
done["errors"].append(r.error or r.status)
|
|
# paced, so as not to run out of local ports
|
|
time.sleep(0.02)
|
|
|
|
clients = [threading.Thread(target=client) for _ in range(4)]
|
|
for c in clients:
|
|
c.start()
|
|
|
|
for i in range(3):
|
|
time.sleep(0.5)
|
|
before = server.output().count("taken over")
|
|
t.write_config("takeover", conf)
|
|
t.http(f"http://127.0.0.1:{adm}/R")
|
|
deadline = time.time() + 10
|
|
while time.time() < deadline and server.output().count("taken over") < before + 3:
|
|
time.sleep(0.05)
|
|
# the old services stop within a second of each reload
|
|
time.sleep(1.5)
|
|
stop.set()
|
|
for c in clients:
|
|
c.join(20)
|
|
|
|
taken = server.output().count("Accepting connections, taken over")
|
|
t.eq(9, taken, "each reload hands the three listening sockets over")
|
|
t.ok(f"requests go on through the reloads ({done['ok']})") if done["ok"] > 100 else \
|
|
t.fail("requests go on through the reloads", "more than 100", done["ok"])
|
|
t.eq(0, done["failed"], "no request is refused or reset by a reload "
|
|
f"(first errors: {done['errors']})")
|
|
|
|
# a changed port is a new socket: the old one closes, the new one opens
|
|
moved = t.free_port()
|
|
t.write_config("takeover", conf.replace(f"proxy -p{proxy}", f"proxy -p{moved}"))
|
|
t.http(f"http://127.0.0.1:{adm}/R")
|
|
t.ok("a service moved to another port listens there") if t.wait_port(moved) else \
|
|
t.fail("a service moved to another port listens there", "open", "closed")
|
|
deadline = time.time() + 5
|
|
while time.time() < deadline and t.wait_port(proxy, timeout=0.2):
|
|
time.sleep(0.2)
|
|
with socket.socket() as s:
|
|
s.settimeout(1)
|
|
refused = s.connect_ex(("127.0.0.1", proxy)) != 0
|
|
t.ok("and its old port is closed") if refused else \
|
|
t.fail("and its old port is closed", "refused", "accepted")
|