Skip to content

resolve_input blocks the event loop, and the resolver stack duplicates ord-schema's #825

Description

@skearnes

Two related findings from reviewing ord-schema#920, which bounded the resolver requests on that side.

1. /v1/resolve_input runs a blocking call on the event loop

resources/v1/utilities.py:61-68 is async def, but resolvers.resolve_input is imported from ord_schema and is fully synchronous — it performs urllib requests to PubChem, CIR, and OPSIN inline. Calling it directly from a coroutine stalls the entire event loop for the duration, not just that request.

The worst case is bounded but large. resolve_input on the "[AMOUNT] of [SOLUTE] in [SOLVENT]" form builds two compounds, and each falls through up to three resolvers. Once ord-schema#920 lands, that is 2 x 3 x 10 s = 60 s of full event-loop stall; today, with no timeouts in the released ord-schema, it is unbounded.

This looks like an oversight rather than a decision, because the codebase already knows the pattern: run_in_threadpool is used in about ten places across domain/reactions.py, domain/datasets.py, and services/pb_utils.py, and the sibling endpoint resolve-compound immediately below correctly awaits an async resolver. Wrapping the call in run_in_threadpool is the fix.

2. services/resolvers.py is a second resolver stack

services/resolvers.py implements name resolution independently of ord_schema.resolvers, on httpx.AsyncClient. It is not strictly worse — fanning out concurrently with asyncio.as_completed and cancelling the losers is a genuine latency win over ord-schema's sequential chain, and the alru_cache is useful. But it has drifted in three ways worth correcting:

A dead resolver. _emolecules_resolve requests lookup?q=, the endpoint ord-schema documents as permanently unusable: it always replies __END__, and the current eMolecules API requires authentication. One of the three concurrent branches cannot ever succeed, so this is a guaranteed-failing request per lookup.

No response size cap. response.raise_for_status().text buffers whatever arrives. A resolver answers with a single SMILES, so an error page or redirect loop is buffered in full.

Timeouts bound each operation, not the request. httpx defaults to 5 s, so this is not unbounded — but that budget is per-chunk, and it resets whenever bytes arrive. A server trickling data holds the connection indefinitely. This is precisely the failure mode ord-schema#920 addresses, and the fix there needed a wall-clock deadline plus read1, because a per-operation timeout cannot express a request ceiling on its own.

Suggested direction

Consolidating on ord_schema.resolvers via run_in_threadpool fixes both findings at once and leaves one resolver implementation to maintain. The tradeoff to weigh is that ord-schema resolves sequentially, so a slow PubChem delays the fallback rather than racing it — if the concurrent fan-out is worth keeping, the alternative is keeping this module but adding a size cap, a request-wide deadline, and dropping the eMolecules branch.

Either way, finding 1 is worth fixing on its own and is a two-line change.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions