fix: --stall-after Budget der Rettungsstation client-kumulativ zählen
Der Testschalter zählte bisher pro Anfrage, sodass eine Lampe nach ihrem Empfangs-Timeout einfach per Range neu verband und ein frisches Budget bekam – am EVK reichten fünf Reconnects, um trotz --stall-after das komplette Image zu übertragen. Station verfolgt die ausgelieferten Bytes jetzt pro Client-IP über alle Anfragen hinweg (Station.reserve_stall_bytes, unter dem bestehenden Lock), sodass kein erneutes Verbinden das Budget auffüllt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
2f25714248
commit
3f27ef4b26
+42
-23
@@ -61,16 +61,35 @@ class Station:
|
||||
self.since = now_iso()
|
||||
self.served = 0
|
||||
self.recent: list[dict] = []
|
||||
# Cumulative bytes already delivered to each client while --stall-after is active,
|
||||
# keyed by client IP. A lamp whose download stalls re-opens a fresh connection and
|
||||
# resumes with Range right where it left off, so a per-request counter would hand
|
||||
# every reconnect a brand-new --stall-after allowance and the stall would never
|
||||
# actually stick (see reserve_stall_bytes below).
|
||||
self.stall_sent: dict[str, int] = {}
|
||||
# ThreadingHTTPServer runs one handler thread per connection, and several lamps
|
||||
# can be downloading at once. Every mutation of served/recent and the snapshot
|
||||
# that gets serialized to disk must happen under this same lock, or concurrent
|
||||
# requests lose increments and stomp on each other's status.json writes.
|
||||
# can be downloading at once. Every mutation of served/recent/stall_sent and the
|
||||
# snapshot that gets serialized to disk must happen under this same lock, or
|
||||
# concurrent requests lose increments and stomp on each other's status.json writes.
|
||||
self.lock = threading.RLock()
|
||||
self.write_state()
|
||||
|
||||
def image_path(self, target: str) -> Path:
|
||||
return self.image_dir / f"ninlight_{target}_ota.img"
|
||||
|
||||
def reserve_stall_bytes(self, ip: str, n: int) -> int:
|
||||
"""How many of the next `n` bytes this client may still receive under --stall-after.
|
||||
|
||||
Tracks bytes cumulatively per client IP, across requests, so a stalled connection
|
||||
that the lamp abandons and resumes via Range does not get a fresh budget just
|
||||
because it is a new TCP connection. Only called when stall_after is set.
|
||||
"""
|
||||
with self.lock:
|
||||
sent_so_far = self.stall_sent.get(ip, 0)
|
||||
allowed = max(0, min(n, self.stall_after - sent_so_far))
|
||||
self.stall_sent[ip] = sent_so_far + allowed
|
||||
return allowed
|
||||
|
||||
def record(self, ip: str, target: str, version: str, nbytes: int, status: str) -> None:
|
||||
with self.lock:
|
||||
if status == "ok":
|
||||
@@ -175,31 +194,31 @@ class Handler(BaseHTTPRequestHandler):
|
||||
sent = 0
|
||||
status = "ok"
|
||||
remaining = size - start
|
||||
client_ip = self.client_address[0]
|
||||
try:
|
||||
with path.open("rb") as f:
|
||||
f.seek(start)
|
||||
while sent < remaining:
|
||||
if self.station.stall_after is not None and sent >= self.station.stall_after:
|
||||
# test aid: hold the connection open and send nothing more, so the
|
||||
# firmware's stall handling can be exercised on real hardware. We
|
||||
# still notice if the peer gives up and closes its side, so a test
|
||||
# (or an impatient client) that hangs up is recorded as "aborted"
|
||||
# instead of leaking the thread forever.
|
||||
while True:
|
||||
readable, _, _ = select.select([self.connection], [], [], 1.0)
|
||||
if readable:
|
||||
try:
|
||||
peek = self.connection.recv(1, socket.MSG_PEEK)
|
||||
except OSError:
|
||||
peek = b""
|
||||
if not peek:
|
||||
raise ConnectionResetError("peer closed during stall")
|
||||
read_size = CHUNK
|
||||
read_size = min(CHUNK, remaining - sent)
|
||||
if self.station.stall_after is not None:
|
||||
# Don't overshoot the stall boundary within a single read: without
|
||||
# this a whole CHUNK could be sent before the check above ever
|
||||
# notices we've passed --stall-after.
|
||||
read_size = min(read_size, max(1, self.station.stall_after - sent))
|
||||
# Reserve against this client's cumulative budget, not just this
|
||||
# request's byte count - see Station.reserve_stall_bytes.
|
||||
read_size = self.station.reserve_stall_bytes(client_ip, read_size)
|
||||
if read_size == 0:
|
||||
# test aid: hold the connection open and send nothing more, so
|
||||
# the firmware's stall handling can be exercised on real
|
||||
# hardware. We still notice if the peer gives up and closes its
|
||||
# side, so a test (or an impatient client) that hangs up is
|
||||
# recorded as "aborted" instead of leaking the thread forever.
|
||||
while True:
|
||||
readable, _, _ = select.select([self.connection], [], [], 1.0)
|
||||
if readable:
|
||||
try:
|
||||
peek = self.connection.recv(1, socket.MSG_PEEK)
|
||||
except OSError:
|
||||
peek = b""
|
||||
if not peek:
|
||||
raise ConnectionResetError("peer closed during stall")
|
||||
chunk = f.read(read_size)
|
||||
if not chunk:
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user