fix: pass a timeout to the metadata heartbeat request - #3361
fix: pass a timeout to the metadata heartbeat request#3361RudraDudhat2509 wants to merge 1 commit into
Conversation
Without it the post could block forever, so the Timeout handler below it and the retry in _ping never ran. Fixes Netflix#3360
Greptile SummaryThe PR prevents metadata heartbeat requests from blocking indefinitely by applying finite connect and read timeouts, allowing the existing retry path to handle stalled services.
Confidence Score: 5/5The PR appears safe to merge with no actionable correctness or security issues identified. The heartbeat now exits stalled network reads through the existing exception and retry flow, and the added tests cover both timeout configuration and error translation.
|
| Filename | Overview |
|---|---|
| metaflow/metadata_provider/heartbeat.py | Adds finite request timeouts while preserving the existing heartbeat exception and retry behavior; no actionable defect identified. |
| test/unit/test_metadata_heartbeat.py | Adds focused regression tests confirming that a positive timeout is supplied and timeout failures reach the existing heartbeat exception path. |
Reviews (1): Last reviewed commit: "fix: pass a timeout to the metadata hear..." | Re-trigger Greptile
There was a problem hiding this comment.
Reviewed the heartbeat failure path against master and #3360.
The root cause checks out: _heartbeat() calls requests.post without a timeout, so a peer that accepts the connection but never responds can block indefinitely. In that state the existing Timeout -> HeartBeatException -> _ping() retry/backoff path is never reached.
Adding a finite (connect, read) timeout makes the existing failure handling reachable without changing the retry logic, successful heartbeat path, or wait_time_in_seconds behavior. The regression test also does the right thing by asserting that a finite timeout exists without coupling the test to the exact timeout constant.
One minor observation: ConnectTimeout is still caught by the earlier ConnectionError branch, but it still becomes HeartBeatException and retries correctly, so that is pre-existing and non-blocking.
I don't see a blocking issue here. LGTM.
Shriprasad-P
left a comment
There was a problem hiding this comment.
Reviewed the heartbeat failure path against master and #3360.
The root cause checks out: _heartbeat() calls requests.post without a timeout, so a peer that accepts the connection but never responds can block indefinitely. In that state the existing Timeout -> HeartBeatException -> _ping() retry/backoff path is never reached.
Adding a finite (connect, read) timeout makes the existing failure handling reachable without changing the retry logic, successful heartbeat path, or wait_time_in_seconds behavior. The regression test also does the right thing by asserting that a finite timeout exists without coupling the test to the exact timeout constant.
One minor observation: ConnectTimeout is still caught by the earlier ConnectionError branch, but it still becomes HeartBeatException and retries correctly, so that is pre-existing and non-blocking.
I don't see a blocking issue here. LGTM.
PR Type
Summary
The metadata heartbeat now times out instead of blocking forever when the service accepts the connection but stops responding, so
_pingcan retry and back off like it was already written to.Issue
Fixes #3360
Reproduction
Runtime: local
Commands to run:
Where evidence shows up:
Before (error / log snippet)
After (evidence that fix works)
The underlying hang, against a socket that accepts and never replies:
Root Cause
_heartbeatposted without a timeout. requests has no default timeout, so if the metadata service accepts the connection but never replies, the post blocks indefinitely.Two things follow from that. The
except requests.exceptions.Timeouthandler directly below the call is unreachable, since nothing can ever raise it. And_ping, which is the retry loop, only retries when_heartbeatraisesHeartBeatException:A call that neither returns nor raises never reaches that, so the retry counter and the
1.5**retry_counterbackoff never run. The heartbeat thread stalls and the run stops heartbeating without surfacing anything.Why This Fix Is Correct
The handler for this case already exists, it just could not be reached. Passing a timeout makes the existing
Timeoutbranch reachable, which raisesHeartBeatException, which is what_pingalready knows how to retry. No new error paths are introduce by this fix, and there is no change to the retry logic.(3.05, 10)is connect and read. 3.05 for connect follows the requests guidance of sitting just above a multiple of 3 so it does not line up with TCP retransmit windows.Failure Modes Considered
_pingcatchesHeartBeatExceptionand backs off before pinging again. 10s read is well above a normal heartbeat response.Timeouthandler and its error message were written for.default_frequency_secs, but the interval is server controlled throughwait_time_in_seconds, so coupling to it would be fragile. Kept it a constant.Tests
test_heartbeat_post_passes_a_timeoutis the regression test, it fails on master. It asserts a positive timeout is passed rather than asserting a specific constant, so it does not lock in the value.test_heartbeat_timeout_raises_heartbeat_exceptionpasses either way. It documents that a timing out request surfaces asHeartBeatExceptionso_pingcan retry.Full
test/unitrun is unchanged against master: 14 failed, 165 errors both before and after, all from optional deps missing locally, with 396 passing before and 398 after. Ran under WSL Ubuntu since metaflow importsfcntland cannot be imported on Windows. black is clean on both files.Non-Goals
Not touching the AWS sandbox STS call in
aws_client.py, that is #2924 and has a PR open. Did not add a config knob for the timeout, and did not change the retry or backoff logic. tried to keep the change as scoped and minimal as possibleAI Tool Usage
Claude Sonnet 5 was used. It wrote the change in
heartbeat.pyand the two tests, and found the bug while grepping forrequestscalls with no timeout. I reviewed all of it, ran the tests both ways, and can explain the causal chain and why the timeout value was chosen. other than that i ran all the setup and the code manually to see there was no degradation.