From 47928706815fbf4d3df597bdeed750ea2b753ec8 Mon Sep 17 00:00:00 2001 From: mesutoezdil Date: Sat, 13 Jun 2026 12:00:26 +0200 Subject: [PATCH 1/5] fix(sbom): release lock before sleeping in _rate_limit time.sleep was called inside the _rate_lock block, blocking all threads from checking their own domain rate limit while one thread slept. With _MAX_WORKERS=12 querying crates.io, npm, and pypi concurrently, this made the thread pool effectively serial. Move the sleep outside the lock so threads for different domains can proceed concurrently. --- deploy/sbom/resolve_licenses.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/sbom/resolve_licenses.py b/deploy/sbom/resolve_licenses.py index fbdfc5fa5..56d76ff2f 100644 --- a/deploy/sbom/resolve_licenses.py +++ b/deploy/sbom/resolve_licenses.py @@ -211,9 +211,9 @@ def _rate_limit(domain: str, interval: float = 0.15) -> None: now = time.time() last = _last_request.get(domain, 0) wait = interval - (now - last) - if wait > 0: - time.sleep(wait) - _last_request[domain] = time.time() + _last_request[domain] = now + if wait > 0: + time.sleep(wait) def _get_json(url: str, domain: str) -> dict | None: From e032994506badd60d4deafc84c347206a78ae2f1 Mon Sep 17 00:00:00 2001 From: mesutoezdil Date: Thu, 18 Jun 2026 16:49:58 +0200 Subject: [PATCH 2/5] fix(sbom): reserve next slot in _rate_limit to prevent same-domain races Use time.monotonic() and store next_req = max(now, last + interval) so concurrent same-domain callers each get a unique future slot rather than all sleeping the same amount. Add regression tests for same-domain spacing and different-domain non-blocking. --- deploy/sbom/resolve_licenses.py | 9 +++--- deploy/sbom/test_resolve_licenses.py | 48 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 deploy/sbom/test_resolve_licenses.py diff --git a/deploy/sbom/resolve_licenses.py b/deploy/sbom/resolve_licenses.py index 56d76ff2f..237e61979 100644 --- a/deploy/sbom/resolve_licenses.py +++ b/deploy/sbom/resolve_licenses.py @@ -208,10 +208,11 @@ 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) - _last_request[domain] = now + 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) diff --git a/deploy/sbom/test_resolve_licenses.py b/deploy/sbom/test_resolve_licenses.py new file mode 100644 index 000000000..8b718ba79 --- /dev/null +++ b/deploy/sbom/test_resolve_licenses.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import time +from concurrent.futures import ThreadPoolExecutor + +from resolve_licenses import _rate_limit, _last_request, _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 zip(times, times[1:]): + 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: + domains = ["alpha.example", "beta.example"] + interval = 0.1 + for d in domains: + with _rate_lock: + _last_request.pop(d, None) + + start = time.monotonic() + with ThreadPoolExecutor(max_workers=2) as pool: + list(pool.map(lambda d: _rate_limit(d, interval=interval), domains)) + elapsed = time.monotonic() - start + + assert elapsed < interval * 1.5, f"different-domain calls blocked: {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") From e067839174a00278584c76e6e23a55947ff6b08c Mon Sep 17 00:00:00 2001 From: mesutoezdil Date: Thu, 18 Jun 2026 17:03:33 +0200 Subject: [PATCH 3/5] fix(sbom): wire rate-limit tests into test suite via test:sbom task Rename test_resolve_licenses.py to resolve_licenses_test.py to match repo *_test.py convention. Add test:sbom task to tasks/test.toml and include it in the top-level test depends so mise run test picks it up. --- .../{test_resolve_licenses.py => resolve_licenses_test.py} | 0 tasks/test.toml | 6 +++++- 2 files changed, 5 insertions(+), 1 deletion(-) rename deploy/sbom/{test_resolve_licenses.py => resolve_licenses_test.py} (100%) diff --git a/deploy/sbom/test_resolve_licenses.py b/deploy/sbom/resolve_licenses_test.py similarity index 100% rename from deploy/sbom/test_resolve_licenses.py rename to deploy/sbom/resolve_licenses_test.py 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"] From 7c210df2499e0cae639eb161290180eae9474f2f Mon Sep 17 00:00:00 2001 From: mesutoezdil Date: Thu, 18 Jun 2026 17:20:46 +0200 Subject: [PATCH 4/5] fix(sbom): add SPDX header, fix cross-domain regression test to exercise lock contention --- deploy/sbom/resolve_licenses_test.py | 39 +++++++++++++++++++++------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/deploy/sbom/resolve_licenses_test.py b/deploy/sbom/resolve_licenses_test.py index 8b718ba79..842ce7aea 100644 --- a/deploy/sbom/resolve_licenses_test.py +++ b/deploy/sbom/resolve_licenses_test.py @@ -1,9 +1,13 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + from __future__ import annotations +import threading import time from concurrent.futures import ThreadPoolExecutor -from resolve_licenses import _rate_limit, _last_request, _rate_lock +from resolve_licenses import _last_request, _rate_limit, _rate_lock def test_same_domain_requests_are_spaced() -> None: @@ -27,18 +31,33 @@ def call() -> None: def test_different_domains_do_not_block_each_other() -> None: - domains = ["alpha.example", "beta.example"] + alpha = "alpha2.example" + beta = "beta2.example" interval = 0.1 - for d in domains: - with _rate_lock: - _last_request.pop(d, None) - start = time.monotonic() - with ThreadPoolExecutor(max_workers=2) as pool: - list(pool.map(lambda d: _rate_limit(d, interval=interval), domains)) - elapsed = time.monotonic() - start + 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 elapsed < interval * 1.5, f"different-domain calls blocked: {elapsed:.3f}s" + assert beta_elapsed < interval * 0.5, f"beta blocked for {beta_elapsed:.3f}s" if __name__ == "__main__": From 2511ce050ccfafa05c4cee1e34d1d7bc3fa2853e Mon Sep 17 00:00:00 2001 From: mesutoezdil Date: Thu, 18 Jun 2026 18:24:56 +0200 Subject: [PATCH 5/5] fix(sbom): replace zip with itertools.pairwise to satisfy ruff lint --- deploy/sbom/resolve_licenses_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deploy/sbom/resolve_licenses_test.py b/deploy/sbom/resolve_licenses_test.py index 842ce7aea..7e8ee0b48 100644 --- a/deploy/sbom/resolve_licenses_test.py +++ b/deploy/sbom/resolve_licenses_test.py @@ -3,6 +3,7 @@ from __future__ import annotations +import itertools import threading import time from concurrent.futures import ThreadPoolExecutor @@ -26,7 +27,7 @@ def call() -> None: list(pool.map(lambda _: call(), range(3))) times.sort() - for a, b in zip(times, times[1:]): + for a, b in itertools.pairwise(times): assert b - a >= interval * 0.9, f"gap {b - a:.4f}s < interval {interval}s"