Fix two unclosed-resource leaks on error/retry paths in infer.py - #64
Fix two unclosed-resource leaks on error/retry paths in infer.py#64nazarli-shabnam wants to merge 1 commit into
Conversation
- infer_one: close() the streaming response before retrying on a 502, instead of letting requests hold the connection open while a new request is issued. Under load (502 = server busy) this was leaking a TCP connection per retry, up to MAX_RETRIES times per job, across all concurrent workers — worst-case exactly when connections are already scarce. - start_server: close log_file before raising RuntimeError when the SGLang subprocess exits early. The file handle was only closed in stop_server(), which this failure path never reaches, leaking the handle whenever the server crashes on startup.
|
Both fixes are correct and match the actual
One related leak this PR doesn't cover, in the same function: the generic for attempt in range(MAX_RETRIES):
resp = None
try:
resp = requests.post(
f"{SERVER_URL}/v1/chat/completions",
headers={"Content-Type": "application/json"},
data=json.dumps(payload),
timeout=REQUEST_TIMEOUT,
stream=True,
)
if resp.status_code == 502 and attempt < MAX_RETRIES - 1:
time.sleep(3 * (attempt + 1))
continue
resp.raise_for_status()
result = collect_stream_silent(resp, output_file)
print(f" [{idx}] {name}: {result['tokens']} tokens, {result['decode_time']:.1f}s")
return result
except Exception as e:
if attempt < MAX_RETRIES - 1:
print(f" [{idx}] {name}: retry {attempt + 1}/{MAX_RETRIES} ({e})")
time.sleep(3 * (attempt + 1))
continue
print(f" [{idx}] {name}: FAILED ({e})")
return {"tokens": 0, "decode_time": 0, "text": ""}
finally:
if resp is not None:
resp.close()Closing an already-fully-consumed stream response is a safe no-op, so an unconditional close in |
Summary
Two small, unrelated-to-any-open-work resource leaks in
infer.py, both on error/retry paths (the code that's hardest to exercise in normal testing, so easiest to get wrong).1.
infer_one: streaming response leaked on 502 retry (infer.py,infer_one)The request is made with
stream=True, so the underlying connection isn't released back to the pool until the body is read orresp.close()is called. On a502, the loop jumps straight back to the top and issues a brand newrequests.post(...)without ever closing the previousresp.MAX_RETRIESis 5, andrun()fans this out across--concurrency(default 8) worker threads, so under a flaky/overloaded server — the exact condition a 502 signals — connections leak fastest right when they're scarcest.Fix: call
resp.close()beforecontinue.2.
start_server: log file handle leaked when the SGLang subprocess exits early (infer.py,start_server)The file handle's lifecycle is meant to be managed via
stop_server(), which doesprocess._log_file.close()— butstop_server()is only invoked from the other failure branch (theSERVER_TIMEOUTpath, a few lines below). This early-exitRuntimeErrorpath bypassesstop_server()entirely and never closeslog_file.Fix: call
log_file.close()right after the existingflush(), before raising.Why these and not something already open
I checked this against the repo's open issues/PRs before picking these (there's a lot of existing work on
infer.py: issue #17 and PRs #21/#26/#34/#36/#13 already cover the PDF tmpdir leak,--image_modevalidation in PDF mode, theavg_decodedivision skew, dataset sort order, and hardcoded ngram params). Neither of the two leaks fixed here is mentioned in any open issue or PR, and the fix for each is a single added line with no behavioral change beyond releasing the resource.Changes
infer.py:resp.close()added before the 502-retrycontinueininfer_one.infer.py:log_file.close()added before theRuntimeErrorinstart_server.Test plan
python -m py_compile infer.py— passes