feat(waterdata): add parallel_chunks to control OGC chunk fan-out#341
Draft
thodson-usgs wants to merge 5 commits into
Draft
feat(waterdata): add parallel_chunks to control OGC chunk fan-out#341thodson-usgs wants to merge 5 commits into
thodson-usgs wants to merge 5 commits into
Conversation
ec8269d to
b47e5cc
Compare
The OGC getters chunk a multi-value request only as far as the server's ~8 KB URL limit forces — the fewest sub-requests. But because every sub-request paginates, splitting a large result further is usually quota-neutral, so that conservative default can be needlessly coarse: ten states pulled as one under-limit request page just as many times as ten per-state requests would. Add `waterdata.chunk_granularity(level)`, a context manager that lets a caller who knows their pull is large opt into a finer split — trading the same pages for more, smaller sub-requests (smoother progress, more even concurrency, a smaller unit of retry/resume). The level is "low", "medium", or "high" (typed as `GranularityLevel`, a Literal, so a type checker rejects anything else; an invalid string raises ValueError at the `with`). Each level caps how many sub-chunks a multi-value argument is split into, derived from the default fan-out concurrency (`API_USGS_CONCURRENT`): high = the full width, medium a quarter, low a sixteenth (32 / 8 / 2 by default). Capping the aggressive end at the concurrency width bounds the blast radius so an accidental "high" on a huge list can't explode into thousands of sub-requests. There is no "off" level — not entering the block is off. It is a scoped `with` block, not an env var, because the library can't tell in advance whether a query is large (a short-window query might fit one page, where extra chunks only burn quota). Implementation: a soft `ChunkPlan._refine` pass runs after the hard byte pass; it only ever splits further, so the url_limit invariant holds and it never raises. The resolved per-axis cap is read from a contextvar (Ambient) set by the context manager at plan-construction time. Exported (with the `GranularityLevel` type) from `dataretrieval.waterdata` and the top-level `dataretrieval` package. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
b47e5cc to
0195113
Compare
…t + benchmark Cap chunk_granularity on the plan's TOTAL sub-request count (2/8/32) rather than per multi-value axis, so several multi-value arguments can't multiply past the ceiling. ChunkPlan._refine now splits the largest splittable chunk across every axis round-robin; behavior is identical for the common single-axis query. Add a 'Speeding up large downloads' usage section to the README (Water Data API) with a measured cold-cache benchmark: parallelizing a large paginated pull's sub-requests gave ~6x (production page size, cold) up to ~12x (latency-bound) on 271 Ohio discharge sites. Temper the 'quota-neutral' claim in the docstring, NEWS, and user guide: a finer split is only ~quota-neutral when each sub-request still spans many pages; otherwise each chunk's partial final page adds some requests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename the public context manager chunk_granularity to parallel_chunks and the GranularityLevel type to ParallelChunksLevel (both exported from dataretrieval and dataretrieval.waterdata), plus internals (_resolve_level, _MAX_PARALLEL_CHUNKS, _LEVEL_CAPS, the _parallel_chunks ambient). 'parallel_chunks' names what the knob does — fan a query into more, parallel sub-requests — without colliding with API_USGS_CONCURRENT (the separate in-flight cap). Docstrings, NEWS, user guide, README, and tests updated to match. Pure rename; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dium/high Replace the three-tier "low"/"medium"/"high" enum with a plain positive integer: parallel_chunks(n) fans a call out into n sub-requests. More expressive (any n, not just 2/8/32), precise, and mirrors the int-valued API_USGS_CONCURRENT. Removes ParallelChunksLevel, the _LEVEL_CAPS/_MAX_PARALLEL_CHUNKS constants, and _resolve_level; validation is now an inline positive-int check (rejects 0, negatives, floats, bool, str). n is bounded below by the byte-limit minimum and above by the number of values to split; n=1 is an explicit no-op. Docstrings, NEWS, user guide, README, exports, and tests updated; 2/8/32 remain as documented examples. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion, dedup test) /simplify cleanup: (1) rename ChunkPlan's max_chunks_per_axis -> max_chunks — the cap is on the plan's TOTAL sub-request count, not per axis, so the old name needed apologetic docstrings; dropped them. (2) Validate parallel_chunks(n) with numbers.Integral (matching the max_rows guard in engine.py), so numpy integers are accepted like the sibling validator. (3) Fold the n=1 no-op test into the parametrized arbitrary-n test, removing a duplicated fetch fixture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
Adds
waterdata.parallel_chunks(n)— a context manager to control how finely the OGCwaterdata(and NGWMN) getters split multi-value requests into chunked sub-requests: it fans a call out intonparallel sub-requests.Today the chunker splits a request only as much as the server's ~8 KB URL-byte limit forces — the fewest sub-requests. That is the safe default, but it can be needlessly conservative. Because every sub-request paginates, splitting a large result further costs little or no extra quota as long as each sub-request still spans many pages: ten states pulled as one request then page nearly as many times as ten per-state requests would. In that situation finer chunks buy smoother progress, more even concurrency, and a smaller unit of retry/resume. (When a split instead leaves each sub-request only a page or two, its partial final page is extra — so on a smaller pull finer chunks do add some requests; reserve a large
nfor pulls you know are large.)The library can't tell in advance whether a query is large (ten states over a short window might fit in a single page, where extra chunks would only burn quota), so this is a deliberate, scoped knob the user sets with their own judgment — not automatic, and not a process-wide env var (which would be a quota footgun). Scoping it to a
withblock keeps an aggressive setting from leaking into unrelated calls.Measured speedup (271 Ohio discharge sites,
get_daily, cold cache, eachnon its own time window): a large paginated pull ran ~6× faster at the production page size and up to ~12× faster when per-request latency dominates — the fan-out parallelizes what is otherwise sequential pagination. See the new README section for the table.The dial
parallel_chunks(n)takes a positive integer — the number of sub-requests to fan the whole call out into.2,8, and32are typical values; a non-integer, non-positive value, or aboolraisesValueErrorat thewith.ncaps the plan's total sub-request count — the cartesian product across every multi-value argument combined, not each argument independently — so several multi-value arguments can't multiply past it. The actual count is bounded below by what the ~8 KB URL limit already forces and above by how many values there are to split (so annlarger than the input allows just yields one sub-request per value);n=1asks for no extra fan-out. There is no"off"— not entering the block is off.Cost and the useful range. Each sub-request fetches at least one page, so it costs at least one request against your hourly rate limit — a larger
nspends more quota. And how many sub-requests run at once is capped separately byAPI_USGS_CONCURRENT(default 32), so annbeyond that adds quota without adding parallelism. The useful range is therefore roughly2up toAPI_USGS_CONCURRENT. Fan-out volume and simultaneity stay deliberately independent controls.Exported as
waterdata.parallel_chunksand, for parity withChunkInterrupted, at the top level asdataretrieval.parallel_chunks.Implementation
ChunkPlan._refine(max_chunks)— a soft pass that runs after the existing hard byte pass (_plan). It splits the largest splittable chunk across every axis (round-robin) until the plan's total sub-request count reaches the cap, only ever splitting further (via the shared_split_atprimitive), so theurl_limitinvariant always holds and it never raises. A no-op at cap 0, so the default path is byte-for-byte unchanged (passthrough preserved).nis read from anAmbient(contextvar) set by the context manager, at plan-construction time insidemulti_value_chunked's wrapper — so a laterresume()(which re-issues already-planned sub-requests) needs no extra snapshot.parallel_chunks(n)validatesninline (a positiveint, rejectingbool/float/str/None) and publishes it on the ambient;ChunkPlanreads that plain int asmax_chunks. There is no level→cap lookup table — the value the user passes is the cap.Tests & checks
tests/waterdata_chunking_test.py, plus an export-surface test; covers the cap→pieces ramp/saturation (with cover-partition checks), support for an arbitraryn(not just 2/8/32), the total-cap product bound across multiple multi-value axes, the guardrail on long axes, byte-budget preservation, filter-axis + multi-axis behavior,n-validation (rejecting0/negative/float/str/None/bool),n=1as an explicit no-op, context-manager scoping/nesting, and the passthrough-unchanged default.ruff check,ruff format --check, andmypy --strictall clean.Note
The dial has been through a few shapes: an
off/1–5/maxscale, a per-axis cap derived from the concurrency width, then a fixed"low"/"medium"/"high"enum — and is now a plain integern, the number of sub-requests to fan out into. The integer is more expressive than three arbitrary tiers, gives precise control, and mirrors the int-valuedAPI_USGS_CONCURRENT;2/8/32survive only as documented examples. The knob was also renamedchunk_granularity→parallel_chunks: "parallel_chunks" says what it does — fan a query into more, parallel sub-requests — without colliding withAPI_USGS_CONCURRENT, the separate in-flight cap. Still a draft.🤖 Generated with Claude Code