Skip to content

Stop the ENCODE sync from aborting partway on a 600-second fetch timeout — Closes #105 - #107

Merged
conradbzura merged 2 commits into
masterfrom
105-encode-fetch-timeout
Aug 12, 2026
Merged

Stop the ENCODE sync from aborting partway on a 600-second fetch timeout — Closes #105#107
conradbzura merged 2 commits into
masterfrom
105-encode-fetch-timeout

Conversation

@conradbzura

Copy link
Copy Markdown
Collaborator

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_encode clears 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, inside fetch_encode_metadata rather than at import. A malformed value then fails the sync that reads it instead of the import of cfdb.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 0 would 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_read would bound inactivity instead and not need revisiting as ENCODE grows; src/cfdb/services/drs.py:367 already uses that shape for a streaming download. One hour was the deliberate choice here.

Report how far a timeout got

Catch asyncio.TimeoutError explicitly, log the row count reached and the budget, and re-raise unchanged so the caller's handling is unaffected. The previous handler caught only aiohttp.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_metadata had 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.

# Test Suite Given When Then Coverage Target
1 tests/test_encode.py No timeout override The fetch drains a response The request is bounded by at least an hour The regression guard for the 600-second literal
2 tests/test_encode.py The timeout variable set to two hours The fetch drains a response The request is bounded by that value Env to parse to request as one chain
3 tests/test_encode.py No timeout variable in the environment The fetch drains a response The request is bounded by the one-hour default The fallback path
4 tests/test_encode.py The variable set to a non-integer, zero, or a negative The fetch is drained It raises naming the variable and issues no request A non-positive total means no timeout in aiohttp
5 tests/test_encode.py A header plus two data lines The fetch drains them One dict per data line, keyed by column The parse loop, and that the stub is faithful
6 tests/test_encode.py A body carrying only a header The fetch drains it No rows and no error An empty corpus is distinguishable from a broken response
7 tests/test_encode.py Blank lines between and after data lines The fetch drains them Only populated rows are yielded A real TSV ends with a trailing newline
8 tests/test_encode.py Lines ending with CRLF The fetch drains them Values carry no carriage return A stray carriage return would corrupt every row's last column
9 tests/test_encode.py A stream that times out after two rows The fetch drains it It logs the count reached and re-raises unchanged The new handler
10 tests/test_encode.py A metadata endpoint returning HTTP 503 The fetch is drained It raises naming the status The previously untested non-200 branch
11 tests/test_encode.py A request raising a client error The fetch is drained It raises naming the network error The previously untested handler

Suite: 971 passing. Both guards mutation-checked — restoring total=600 fails 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.

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.
@conradbzura conradbzura self-assigned this Aug 12, 2026
@conradbzura
conradbzura marked this pull request as ready for review August 12, 2026 21:37
@conradbzura
conradbzura merged commit 5e08ca0 into master Aug 12, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stop the ENCODE sync from aborting partway on a 600-second fetch timeout

1 participant