Skip to content

feat: cap concurrent requests per FDSN provider at 3 per run - #24

Merged
jsteinberg34 merged 4 commits into
mainfrom
feat/cap-per-provider-concurrency
Oct 4, 2026
Merged

jsteinberg34 merged 4 commits into
mainfrom
feat/cap-per-provider-concurrency

Conversation

@jsteinberg34

@jsteinberg34 jsteinberg34 commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

A run now keeps at most 3 requests in flight against any single FDSN provider, however many boxes download at once or how many threads each uses. Requests over the limit wait for a slot; nothing fails because of it. Released as 0.6.0.

Why

max_workers (boxes in flight) and threads_per_client (MassDownloader threads per provider) multiply when overlapping boxes share a provider. At the defaults that is up to 9 concurrent requests at one archive, and up to 30 at MAX_WORKERS_CAP. SCEDC publishes a limit of 3 concurrent sessions and states that exceeding it results in IP blacklisting (source), so any Southern California event could breach it. The source also claimed FDSN providers have "no published rate limit", which was wrong.

Lowering a default is not a good fix. On the Shenzhou-15 case, both compliant single-knob settings (max_workers=3, threads_per_client=1 and max_workers=1, threads_per_client=3) took roughly 200 s against 71 s at the old defaults.

How

  • One Client per provider is already built per run and shared by every box and thread. Right after construction, its three request methods (get_stations, get_stations_bulk, get_waveforms_bulk) are wrapped to take a permit from a per-provider BoundedSemaphore(3). That covers every request a run makes: inventory, availability, waveforms and StationXML.
  • ObsPy is not modified and only its public API is used. Instance wrapping rather than a subclass keeps the tests' groundtrack.download.Client monkeypatch working and passes MassDownloader's isinstance(..., Client) check.
  • The gate is reentrant per thread, because get_waveforms_bulk(attach_response=True) calls self.get_stations, which would otherwise deadlock once every permit is held.
  • PROVIDER_CONCURRENCY_LIMIT = 3 lives in providers.py and applies uniformly to every provider. The limit is per run, and per server rather than per provider name: permits are keyed by the client's base_url, so IRISPH5 (a distinct provider on EarthScope's server) shares EarthScope's allowance instead of doubling it.
  • max_workers and threads_per_client keep their defaults and meaning. What is downloaded is unchanged.
  • Manifests gain a run-level provider_concurrency_limit field.
  • Comments, the max_workers cap warning, README and docs are corrected (no more "connections per data centre" or "no published rate limit").

Testing

  • 233 tests pass. No existing test needed a behavioural change; one existing test's docstring was reworded to drop the old "connections" framing.
  • New tests: the limit holds across threads and across all three methods, providers are limited independently, permits are released on exceptions, nested calls do not deadlock, and arguments and results pass through unchanged. An end-to-end download_boxes() run with 6 overlapping boxes at max_workers=4, threads_per_client=3 never exceeds 3 in flight and downloads the same stations as a sequential run. Two providers reporting the same server share one limit.
  • Checked against broken gates: with no gate the end-to-end test fails (peak 5), with a non-reentrant gate the deadlock test fails after 3 s instead of hanging, and with permits keyed by provider name the shared-server test fails (peak 6).
  • A contract test scans ObsPy's mass_downloader source and fails if it ever calls a Client method outside the gated three.
  • CI passes on Python 3.10 and 3.13 against the latest ObsPy, so the contract test also covers releases newer than 1.5.0.
  • The Sphinx docs build was not run locally (Sphinx is not installed in the dev environment).

Cost

One paired trial on Shenzhou-15:

elapsed files / MB
plain ObsPy MassDownloader, its own defaults 92 s 229 / 29.1
groundtrack 0.6.0 104 s 390 / 49.1
groundtrack 0.5.0 defaults (median of 8, exceeded SCEDC's limit) 71 s 390 / 49.1

Identical data, about 1.5x slower than the old non-compliant defaults, and roughly time-neutral with plain ObsPy at equal per-provider load while fetching 1.7x the data. Single trial, so treat the ratios as indicative.

@codecov

codecov Bot commented Oct 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@jsteinberg34
jsteinberg34 merged commit 6d90007 into main Oct 4, 2026
5 checks passed
@jsteinberg34
jsteinberg34 deleted the feat/cap-per-provider-concurrency branch October 4, 2026 23:51
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.

1 participant