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>
100 lines
3.6 KiB
Python
100 lines
3.6 KiB
Python
"""Reloading the configuration: a file with an error leaves the configuration
|
|
in force, services and users, as it was; a good one replaces it, while the
|
|
connections of the old services finish undisturbed."""
|
|
|
|
import threading
|
|
import time
|
|
|
|
|
|
def config(origin, proxy, adm, user, extra=""):
|
|
return f"""
|
|
log
|
|
auth iponly
|
|
allow *
|
|
http data * /data
|
|
httpsrv -p{origin}
|
|
|
|
flush
|
|
users {user}
|
|
auth strong
|
|
allow *
|
|
proxy -p{proxy}
|
|
|
|
flush
|
|
auth iponly
|
|
allow *
|
|
admin -p{adm}
|
|
{extra}
|
|
"""
|
|
|
|
|
|
def run(t):
|
|
origin = t.free_port()
|
|
first = t.free_port()
|
|
second = t.free_port()
|
|
adm = t.free_port()
|
|
|
|
server = t.start("reload", config(origin, first, adm, "alice:CL:one"),
|
|
ports=[origin, first, adm])
|
|
url = f"http://127.0.0.1:{origin}/data?size=1000"
|
|
proxy = f"127.0.0.1:{first}"
|
|
|
|
def as_user(user, password, through=proxy):
|
|
return t.http(url, proxy=through, proxy_auth=(user, password)).status
|
|
|
|
t.eq(200, as_user("alice", "one"), "the first configuration lets alice in")
|
|
|
|
def reload_and_wait(expect_failure=False):
|
|
before = server.output()
|
|
accepting = before.count("Accepting connections")
|
|
t.http(f"http://127.0.0.1:{adm}/R")
|
|
deadline = time.time() + 10
|
|
while time.time() < deadline:
|
|
out = server.output()
|
|
if expect_failure and "Unknown command" in out[len(before):]:
|
|
return out
|
|
if not expect_failure and out.count("Accepting connections") >= accepting + 3:
|
|
return out
|
|
time.sleep(0.05)
|
|
return server.output()
|
|
|
|
# --- a file with an error: nothing changes --------------------------
|
|
t.write_config("reload", config(origin, first, adm, "bob:CL:two", "nosuchcommand"))
|
|
out = reload_and_wait(expect_failure=True)
|
|
t.contains(out, "Unknown command", "the broken configuration is refused")
|
|
time.sleep(1.5) # what a switch would take: the old services stop within a second
|
|
t.eq(200, as_user("alice", "one"), "after a failed reload alice still gets in")
|
|
t.ne(200, as_user("bob", "two"), "and bob, from the refused file, does not")
|
|
|
|
# --- a good file: the new configuration takes over -------------------
|
|
slow = {}
|
|
|
|
def download():
|
|
# about two seconds: 2 MB in 64 KB blocks, 60 ms apart
|
|
r = t.http(f"http://127.0.0.1:{origin}/data?size=2000000&block=65536&delay=60",
|
|
proxy=proxy, proxy_auth=("alice", "one"))
|
|
slow["status"], slow["length"] = r.status, r.length
|
|
|
|
th = threading.Thread(target=download)
|
|
th.start()
|
|
time.sleep(0.3)
|
|
|
|
t.write_config("reload", config(origin, second, adm, "bob:CL:two"))
|
|
reload_and_wait()
|
|
t.ok("the new services start") if t.wait_port(second) else \
|
|
t.fail("the new services start", f"port {second} open", "closed")
|
|
moved = f"127.0.0.1:{second}"
|
|
t.eq(200, as_user("bob", "two", moved), "the new configuration lets bob in")
|
|
t.ne(200, as_user("alice", "one", moved), "and not alice, who is gone from it")
|
|
|
|
# the old service stops accepting, its port closes
|
|
deadline = time.time() + 5
|
|
while time.time() < deadline and t.wait_port(first, timeout=0.2):
|
|
time.sleep(0.2)
|
|
t.ok("the old service stops listening") if not t.wait_port(first, timeout=0.2) else \
|
|
t.fail("the old service stops listening", f"port {first} closed", "open")
|
|
|
|
th.join(20)
|
|
t.eq(200, slow.get("status"), "a download through the old service finishes")
|
|
t.eq(2000000, slow.get("length"), "with the whole answer")
|