diff --git a/deploy/sbom/resolve_licenses.py b/deploy/sbom/resolve_licenses.py index fbdfc5fa5..237e61979 100644 --- a/deploy/sbom/resolve_licenses.py +++ b/deploy/sbom/resolve_licenses.py @@ -208,12 +208,13 @@ def _rate_limit(domain: str, interval: float = 0.15) -> None: with _rate_lock: - now = time.time() - last = _last_request.get(domain, 0) - wait = interval - (now - last) - if wait > 0: - time.sleep(wait) - _last_request[domain] = time.time() + now = time.monotonic() + last = _last_request.get(domain, 0.0) + next_req = max(now, last + interval) + _last_request[domain] = next_req + wait = next_req - now + if wait > 0: + time.sleep(wait) def _get_json(url: str, domain: str) -> dict | None: diff --git a/deploy/sbom/resolve_licenses_test.py b/deploy/sbom/resolve_licenses_test.py new file mode 100644 index 000000000..7e8ee0b48 --- /dev/null +++ b/deploy/sbom/resolve_licenses_test.py @@ -0,0 +1,68 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import itertools +import threading +import time +from concurrent.futures import ThreadPoolExecutor + +from resolve_licenses import _last_request, _rate_limit, _rate_lock + + +def test_same_domain_requests_are_spaced() -> None: + domain = "test.same-domain.example" + with _rate_lock: + _last_request.pop(domain, None) + + interval = 0.05 + times: list[float] = [] + + def call() -> None: + _rate_limit(domain, interval=interval) + times.append(time.monotonic()) + + with ThreadPoolExecutor(max_workers=3) as pool: + list(pool.map(lambda _: call(), range(3))) + + times.sort() + for a, b in itertools.pairwise(times): + assert b - a >= interval * 0.9, f"gap {b - a:.4f}s < interval {interval}s" + + +def test_different_domains_do_not_block_each_other() -> None: + alpha = "alpha2.example" + beta = "beta2.example" + interval = 0.1 + + now = time.monotonic() + with _rate_lock: + _last_request[alpha] = now # alpha must sleep for ~interval + _last_request.pop(beta, None) # beta is free + + ready = threading.Event() + + def call_alpha() -> None: + ready.set() + _rate_limit(alpha, interval=interval) + + t = threading.Thread(target=call_alpha) + t.start() + ready.wait() + time.sleep(0.01) # let alpha enter its sleep + + beta_start = time.monotonic() + _rate_limit(beta, interval=interval) + beta_elapsed = time.monotonic() - beta_start + + t.join() + + assert beta_elapsed < interval * 0.5, f"beta blocked for {beta_elapsed:.3f}s" + + +if __name__ == "__main__": + test_same_domain_requests_are_spaced() + print("same-domain spacing: ok") + test_different_domains_do_not_block_each_other() + print("different-domain non-blocking: ok") diff --git a/tasks/test.toml b/tasks/test.toml index d2e8d642d..3ee4b6ab5 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -5,13 +5,17 @@ [test] description = "Run all tests (Rust + Python)" -depends = ["test:rust", "test:python", "test:install-sh", "test:packaging-assets", "test:docs-website"] +depends = ["test:rust", "test:python", "test:sbom", "test:install-sh", "test:packaging-assets", "test:docs-website"] ["test:docs-website"] description = "Test the docs-website sync script" # --no-project skips the workspace (maturin) build; --with supplies pytest and # the script's runtime dep (PyYAML), which live outside the project env. run = "uv run --no-project --with pytest --with pyyaml pytest tasks/scripts/sync_docs_website_test.py" + +["test:sbom"] +description = "Run SBOM tooling tests" +run = "uv run --no-project --with pytest pytest -o \"python_files=*_test.py\" deploy/sbom/" hide = true ["test:install-sh"]