Stop the ENCODE sync from aborting partway on a 600-second fetch timeout — Closes #105 - #107
Merged
Merged
Conversation
The fetch bounded its request with a 600-second total, which covers the whole streamed response rather than inactivity. Every row is transformed and inserted as it streams, so the wall clock tracks insert throughput against the target database, not network latency, and ten minutes was not enough against DocumentDB: the dev sync aborted around 230,000 of roughly 810,000 rows. Because the DCC is cleared before reloading, that left the corpus smaller than it started, and an earlier count of 242,000 is consistent with the same timeout firing at a different point rather than with any deliberate cap. The budget is now an hour and reads from ENCODE_METADATA_TIMEOUT_SECONDS, resolved per call rather than at import so a malformed value fails the sync that uses it instead of the import of this module, which would take down every read the API serves over a knob only the sync touches. A non-positive value is rejected rather than passed through, since aiohttp treats it as no timeout at all and would silently convert a bounded request into an unbounded one. A timeout is also caught explicitly now and logged with the row count it reached, then re-raised unchanged. That count is the one fact separating a budget that is too small from an endpoint that is down, and the previous handler caught only ClientError, so the failure arrived as a bare traceback naming neither.
The fetch had no tests at all. Its parse loop, header handling, non-200 branch and network-error handler were reachable only through the sync, which replaces the whole function with a generator over already-parsed rows, so none of that code had ever run under test. Nothing in the suite modelled a streamed body either. The new stubs mirror the 4DN fakes but differ in two ways the subject demands: the response body is an async iterator of byte lines, so the parser's own decode and newline handling are exercised rather than bypassed, and the session records the kwargs of every request. The 4DN fakes discard those, and no test in the repo asserted on a request kwarg before, but the timeout is a request kwarg and is the behavior under test. The budget is asserted through the fetch rather than against the parser alone. A parser test passes whether or not the value it returns ever reaches the request, which is precisely the gap that let a 600-second literal sit unnoticed. Blank lines and CRLF endings are covered because the upstream is a real TSV served over HTTP: it ends with a trailing newline, and a stray carriage return would attach to every row's last column, turning a format of bed into bed followed by a carriage return that matches no CV lookup.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bound the ENCODE metadata download by a configurable budget instead of a 600-second literal, and report how far a load got when the budget expires.
The fetch streams a hundreds-of-megabyte TSV and transforms and inserts each row as it arrives, so the wall clock tracks insert throughput against the target database rather than network latency.
aiohttp.ClientTimeout(total=...)covers that entire operation, not inactivity, so ten minutes was a cap on corpus size dressed as a network timeout. On dev the sync reached 230,000 of roughly 810,000 rows and aborted; a local run against containerized MongoDB completed all 809,981, because local inserts finish inside the budget. Dev's earlier count of 242,000 is consistent with the same timeout firing at a different point rather than with any deliberate cap.The failure does not self-correct.
_sync_encodeclears the DCC before reloading, so an aborted run leaves fewer documents than it started with — dev is currently at 230,000, down from 242,000.Closes #105
Proposed changes
Resolve the budget per call
Read
ENCODE_METADATA_TIMEOUT_SECONDS, defaulting to one hour, insidefetch_encode_metadatarather than at import. A malformed value then fails the sync that reads it instead of the import ofcfdb.services.encode, which would take down every read the API serves over a knob only the sync touches. It also makes the override observable through the public API, so the tests exercise env → parse → request as one chain rather than poking the parser directly.Reject a non-positive value rather than passing it through: aiohttp treats a non-positive total as no timeout, so a typo reading
0would silently convert a bounded request into an unbounded one — failing in the direction hardest to notice.A total budget still scales with corpus size and insert speed.
sock_readwould bound inactivity instead and not need revisiting as ENCODE grows;src/cfdb/services/drs.py:367already uses that shape for a streaming download. One hour was the deliberate choice here.Report how far a timeout got
Catch
asyncio.TimeoutErrorexplicitly, log the row count reached and the budget, and re-raise unchanged so the caller's handling is unaffected. The previous handler caught onlyaiohttp.ClientError, so a timeout surfaced as a bare traceback — and the row count is the one fact separating "the budget is too small" from "the endpoint is down".Test cases
fetch_encode_metadatahad no tests. Its parse loop, header handling, non-200 branch and network-error handler were reachable only through the sync, which replaces the function wholesale with a generator over already-parsed rows. Nothing in the suite modelled a streamed body either, so the stubs are new: the response body is an async iterator of byte lines, and the session records request kwargs, which the sibling 4DN fakes discard.tests/test_encode.pytests/test_encode.pytests/test_encode.pytests/test_encode.pytests/test_encode.pytests/test_encode.pytests/test_encode.pytests/test_encode.pytests/test_encode.pytests/test_encode.pytests/test_encode.pySuite: 971 passing. Both guards mutation-checked — restoring
total=600fails three tests, ignoring the env var fails four.Not addressed. The issue's expected behavior includes failing "without having discarded the previous data", which needs a staging-collection cutover rather than a timeout change. This reduces how often that bites without removing it.