Skip to content

Commit 64adc3a

Browse files
committed
Allow native server restarts after TIME_WAIT while rejecting live listeners (v100.36)
1 parent e1cb754 commit 64adc3a

7 files changed

Lines changed: 75 additions & 8 deletions

File tree

‎V100_AUDIT.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,11 @@ The updater includes this check before installing, so recovery also works with
124124
older installed packages. Live command/session ownership mismatches still abort
125125
without killing the process or replacing packages. Checkpoints and failure reports
126126
are retained. A successful update is not evidence of a successful weight promotion.
127+
128+
## v100.36 native server restart ports
129+
130+
Native and MTP availability probes now use SO_REUSEADDR and actually listen before
131+
closing the probe. Linux TIME_WAIT after a completed acceptance request no longer
132+
looks like a live server occupying the port. An existing loopback or wildcard
133+
listener still blocks launch, with the port included in the error. No listener is
134+
killed or reused, and no second model is started against an occupied endpoint.

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "rlms"
33
authors = [
44
{name = "Alex Zhang", email = "altzhang@mit.edu"},
55
]
6-
version = "0.1.3+v100.35"
6+
version = "0.1.3+v100.36"
77
description = "Recursive Language Models."
88
readme = "README.md"
99
license = "MIT"

‎rlm/v100/competition.py‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import copy
44
import json
55
import os
6-
import socket
76
import subprocess
87
import sys
98
import time
@@ -19,6 +18,7 @@
1918
from rlm.v100.common import atomic_json, load_profile
2019
from rlm.v100.experiments import SharedLab, judge_duel, load_duel, plan_duel
2120
from rlm.v100.researchers import available_ram_gib, research_task, review_research
21+
from rlm.v100.serving import ensure_local_port_available
2222

2323

2424
def command(root: Path, profile: Path, *arguments: str) -> list[str]:
@@ -85,8 +85,7 @@ def managed_server(profile_path: Path, root: Path, log_path: Path):
8585
yield profile
8686
return
8787
origin = urlparse(profile["runtime"]["base_url"])
88-
with socket.socket() as probe:
89-
probe.bind(("127.0.0.1", origin.port))
88+
ensure_local_port_available(origin.port)
9089
cpu = profile.get("resources", {}).get("device") == "cpu"
9190
minimum = profile.get("resources", {}).get("min_available_ram_gib", 6)
9291
if cpu and available_ram_gib() < minimum:

‎rlm/v100/serving.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,27 @@
22

33
import json
44
import os
5+
import socket
56
from pathlib import Path
67
from urllib.parse import urlparse
78

89
from rlm.v100.common import atomic_json
910
from rlm.v100.protection import execution_hash, file_hash
1011

1112

13+
def ensure_local_port_available(port: int) -> None:
14+
"""Reject live listeners, while allowing a restarted server's TIME_WAIT sockets."""
15+
with socket.socket() as probe:
16+
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
17+
try:
18+
probe.bind(("127.0.0.1", port))
19+
probe.listen(1)
20+
except OSError as error:
21+
raise OSError(
22+
error.errno, f"Local server port {port} unavailable: {error.strerror}"
23+
) from error
24+
25+
1226
def process_identity(pid: int) -> str:
1327
# Linux start time prevents a reused PID being accepted as the protected server.
1428
fields = Path(f"/proc/{pid}/stat").read_text().rsplit(")", 1)[1].split()

‎rlm/v100/speculative.py‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import os
66
import re
77
import shutil
8-
import socket
98
import statistics
109
import subprocess
1110
import sys
@@ -18,6 +17,7 @@
1817
import requests
1918

2019
from rlm.v100.common import atomic_json, load_profile
20+
from rlm.v100.serving import ensure_local_port_available
2121

2222
ASSISTANT_MODEL = "google/gemma-4-12B-it-assistant"
2323
ASSISTANT_REVISION = "46d4c6f13f0ac0ad827b915669b8df9b81c64c51"
@@ -216,8 +216,7 @@ def test_mtp(
216216
value = profile["server"][key]
217217
if value and not Path(value).is_file():
218218
raise FileNotFoundError(value)
219-
with socket.socket() as probe:
220-
probe.bind(("127.0.0.1", 8089))
219+
ensure_local_port_available(8089)
221220
memory = (
222221
subprocess.run(
223222
["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader,nounits"],

‎tests/test_v100_server_ports.py‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Local restart probes must distinguish TIME_WAIT from an occupied listener."""
2+
3+
import errno
4+
import socket
5+
import sys
6+
7+
import pytest
8+
9+
from rlm.v100.serving import ensure_local_port_available
10+
11+
12+
@pytest.mark.parametrize("host", ["127.0.0.1", "0.0.0.0"])
13+
def test_live_listener_still_blocks_reusable_probe(host):
14+
with socket.socket() as listener:
15+
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
16+
listener.bind((host, 0))
17+
listener.listen(1)
18+
port = listener.getsockname()[1]
19+
with pytest.raises(OSError, match=f"port {port} unavailable") as failure:
20+
ensure_local_port_available(port)
21+
assert failure.value.errno == errno.EADDRINUSE
22+
23+
24+
@pytest.mark.skipif(sys.platform != "linux", reason="Linux TIME_WAIT restart regression")
25+
def test_restart_after_server_closes_connection_does_not_need_kernel_timeout():
26+
with socket.socket() as listener, socket.socket() as client:
27+
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
28+
listener.bind(("127.0.0.1", 0))
29+
listener.listen(1)
30+
port = listener.getsockname()[1]
31+
client.connect(("127.0.0.1", port))
32+
accepted, _ = listener.accept()
33+
# Server closes first, so its local endpoint enters TIME_WAIT.
34+
accepted.shutdown(socket.SHUT_WR)
35+
assert client.recv(1) == b""
36+
client.close()
37+
accepted.close()
38+
with socket.socket() as old_probe:
39+
with pytest.raises(OSError) as failure:
40+
old_probe.bind(("127.0.0.1", port))
41+
assert failure.value.errno == errno.EADDRINUSE
42+
ensure_local_port_available(port)
43+
# The probe itself leaves no resident listener behind.
44+
with socket.socket() as restarted:
45+
restarted.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
46+
restarted.bind(("127.0.0.1", port))
47+
restarted.listen(1)

‎tools/upgrade-v100-campaign.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ PY_STOP
5555
uv --no-config pip install --python "$PY" --no-deps --reinstall-package rlms \
5656
"rlms @ git+https://github.com/Mantiee/rlm.git@$REV"
5757
uv --no-config pip check --python "$PY"
58-
uv --no-config pip freeze --python "$PY" > "$ROOT/research/requirements.continual.v10035.txt"
58+
uv --no-config pip freeze --python "$PY" > "$ROOT/research/requirements.continual.v10036.txt"
5959
LAB="$ROOT/bin/v100-continual"
6060
if [[ -e "$LAB" ]]; then cp -p "$LAB" "$LAB.backup-$(date -u +%Y%m%dT%H%M%SZ)"; fi
6161
TMP="$(mktemp "$ROOT/bin/.v100-continual.XXXXXX")"

0 commit comments

Comments
 (0)