Skip to content

Non-finite cluster_eps either 500s or wedges a worker thread forever #379

Description

@lstein

Found while reviewing #376. Not reachable by typing into the Cluster Strength spinner — <input type="number"> sanitizes both Infinity and 1e400 to "" — but reachable from the URL, from any API client, and from a hand-edited config.

cluster_eps is declared as a bare float | None on /umap_data (photomap/backend/routers/umap.py:25) and on /cluster_labels, and as float | None on the set_umap_eps body model (photomap/backend/routers/album.py:26-32). FastAPI/Pydantic coerce inf, Infinity and 1e400 to float("inf"), and nan/NaN to float("nan"). Neither is rejected anywhere downstream.

nan — 500

GET /umap_data/<album>?cluster_eps=nan

resolve_cluster_eps passes it straight through (max(nan, MIN_CLUSTER_EPS) is nan, and while nan > MIN_BUDGETED_EPS is false), so it reaches the fit:

InvalidParameterError: The 'eps' parameter of DBSCAN must be a float in the range (0.0, inf). Got nan instead.

inf — a thread-pool worker that never comes back

GET /umap_data/<album>?cluster_eps=inf

_shrink_eps_to_pair_budget (photomap/backend/cluster_eps.py:163-186) loops:

while eps > MIN_BUDGETED_EPS:
    pairs = int(tree.query_radius(coords, r=eps, count_only=True).sum())
    if pairs <= MAX_NEIGHBOR_PAIRS:
        return eps
    eps *= 0.7

inf * 0.7 is inf, and with r=inf every point is every point's neighbour, so pairs is — over MAX_NEIGHBOR_PAIRS (50,000,000) for any album past ~7,071 images. Neither exit condition can ever be reached. Verified on an 8,000-point cloud: still looping after 20s, and it is a full query_radius pass per iteration, so it burns the asyncio.to_thread worker indefinitely. Enough of these and the server stops answering.

The storage path is worse because it persists: POST /set_umap_eps/ {"eps": 1e400} stores inf, which YAML round-trips as .inf, after which /get_umap_eps raises ValueError: Out of range float values are not JSON compliant and every /umap_data for that album hangs. The album needs its config hand-edited to recover.

Suggested fix

Constrain the value where it enters, rather than defending inside the resolver — #375 is already adding range validation for the same control, so this belongs with it: reject non-finite (and out-of-range) cluster_eps on /umap_data, /cluster_labels and set_umap_eps with a 422. A belt-and-braces math.isfinite guard in _shrink_eps_to_pair_budget would also stop the loop being unbounded on principle.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions