Skip to content

Commit 3f36f6c

Browse files
committed
Tighten full scan polling and document it
Stop polling once the next attempt could not start before the deadline instead of sleeping out the budget, give each request the configured timeout rather than a fixed 30s cap, and document the polling parameters in the README.
1 parent ad138f6 commit 3f36f6c

4 files changed

Lines changed: 53 additions & 21 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
- The polling budget defaults to the SDK's configured `timeout`. Callers can
1616
tune it with the new keyword-only arguments `poll_timeout_seconds`,
1717
`poll_initial_interval_seconds` and `poll_max_interval_seconds`. Each
18-
individual request is capped at 30 seconds or the remaining budget,
19-
whichever is shorter.
18+
request is bounded by the configured `timeout` or the remaining budget,
19+
whichever is shorter, and polling stops early once the next attempt could
20+
not start before the budget runs out.
2021
- Transient failures (HTTP 408/502/503/504, timeouts, dropped connections) are
2122
retried within the budget. Authorization, not-found and quota errors still
2223
raise immediately.

‎README.rst‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,11 @@ Stream a diff between two full scans. Returns a scan diff.
291291

292292
fullscans.stream(org_slug, full_scan_id, use_types=False)
293293
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""
294-
Stream all SBOM artifacts for a full scan.
294+
Stream all SBOM artifacts for a full scan. If the scan is still being analyzed, the SDK polls
295+
cached results until they are ready, backing off from 5 to 10 seconds between attempts, and
296+
raises ``APITimeout`` if they are not ready within the polling budget. Transient failures
297+
(HTTP 408/502/503/504, timeouts, dropped connections) are retried within the budget; other
298+
errors raise immediately.
295299

296300
**Usage:**
297301

@@ -304,11 +308,17 @@ Stream all SBOM artifacts for a full scan.
304308
# With typed response
305309
print(socket.fullscans.stream("org_slug", "full_scan_id", use_types=True))
306310
311+
# Wait up to 10 minutes for analysis to finish
312+
print(socket.fullscans.stream("org_slug", "full_scan_id", poll_timeout_seconds=600))
313+
307314
**PARAMETERS:**
308315

309316
- **org_slug (str)** - The organization name
310317
- **full_scan_id (str)** - The ID of the full scan
311318
- **use_types (bool)** - Whether to return typed response objects (default: False)
319+
- **poll_timeout_seconds (float)** - Keyword-only. Total time to wait for results (default: the SDK's configured ``timeout``)
320+
- **poll_initial_interval_seconds (float)** - Keyword-only. Wait before the first retry (default: 5.0)
321+
- **poll_max_interval_seconds (float)** - Keyword-only. Longest wait between retries (default: 10.0)
312322

313323
fullscans.metadata(org_slug, full_scan_id, use_types=False)
314324
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

‎socketdev/fullscans/__init__.py‎

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -994,33 +994,27 @@ def stream(
994994
deadline = time.monotonic() + timeout
995995
interval = poll_initial_interval_seconds
996996
while True:
997-
remaining = deadline - time.monotonic()
998-
if remaining <= 0:
999-
raise APITimeout("Timed out waiting for full scan results")
997+
retry_error = None
1000998
try:
1001999
response = self.api.do_request(
10021000
path=path,
10031001
method="GET",
1004-
request_timeout=min(30.0, float(request_timeout), remaining),
1002+
request_timeout=min(float(request_timeout), max(deadline - time.monotonic(), 0.001)),
10051003
)
10061004
except APIFailure as error:
10071005
if not error.is_transient_error():
10081006
raise
1009-
remaining = deadline - time.monotonic()
1010-
if remaining <= 0:
1011-
raise APITimeout("Timed out waiting for full scan results") from error
1012-
time.sleep(min(interval, remaining))
1013-
interval = min(poll_max_interval_seconds, interval * 1.5)
1014-
continue
1015-
if response.status_code == 202:
1007+
retry_error = error
1008+
else:
1009+
if response.status_code != 202:
1010+
break
10161011
response.close()
1017-
remaining = deadline - time.monotonic()
1018-
if remaining <= 0:
1019-
raise APITimeout("Timed out waiting for full scan results")
1020-
time.sleep(min(interval, remaining))
1021-
interval = min(poll_max_interval_seconds, interval * 1.5)
1022-
continue
1023-
break
1012+
# Give up now if the next attempt could not start before the deadline,
1013+
# rather than sleeping out the rest of the budget and never polling again.
1014+
if deadline - time.monotonic() <= interval:
1015+
raise APITimeout("Timed out waiting for full scan results") from retry_error
1016+
time.sleep(interval)
1017+
interval = min(poll_max_interval_seconds, interval * 1.5)
10241018

10251019
if response.status_code == 200:
10261020
try:

‎tests/unit/test_full_scan_cached_polling_http.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import time
23

34
import pytest
45

@@ -153,3 +154,29 @@ def test_invalid_configured_request_timeout_fails_before_request(api_fixture, va
153154
with pytest.raises(ValueError):
154155
sdk.fullscans.stream("org", "scan", poll_timeout_seconds=0.1)
155156
assert not api_fixture.requests
157+
158+
159+
def test_gives_up_without_sleeping_when_next_poll_cannot_start(api_fixture):
160+
api_fixture.route("GET", "orgs/org/full-scans/scan", SocketResponse(202, {}))
161+
start = time.monotonic()
162+
with pytest.raises(APITimeout, match="full scan results"):
163+
sdk_for(api_fixture).fullscans.stream(
164+
"org", "scan", poll_timeout_seconds=0.5, poll_initial_interval_seconds=5,
165+
)
166+
assert time.monotonic() - start < 0.4
167+
assert len(api_fixture.requests) == 1
168+
169+
170+
def test_first_request_uses_full_configured_timeout(api_fixture):
171+
api_fixture.route("GET", "orgs/org/full-scans/scan", ndjson())
172+
sdk = sdk_for(api_fixture, timeout=120)
173+
do_request = sdk.api.do_request
174+
timeouts = []
175+
176+
def spy(**kwargs):
177+
timeouts.append(kwargs["request_timeout"])
178+
return do_request(**kwargs)
179+
180+
sdk.api.do_request = spy
181+
sdk.fullscans.stream("org", "scan")
182+
assert timeouts and timeouts[0] > 100

0 commit comments

Comments
 (0)