Skip to content

Commit 58aca1d

Browse files
fix: use monotonic clock for file processing timeout (openai#3748)
## Summary - Use `time.monotonic()` for elapsed-time deadlines in both synchronous and asynchronous file-processing polling. - Add regression coverage for a wall-clock rollback in both resource variants. ## Bug `Files.wait_for_processing()` and its asynchronous counterpart used `time.time()` to measure elapsed time. If the system wall clock moves backwards while a file is still processing, the calculated elapsed time also moves backwards and the timeout can be postponed indefinitely. ### Reproduction With `max_wait_seconds=1`, `poll_interval=5`, and a wall clock that changes from `100` to `90` after the first poll: - Expected: raise the timeout error once the monotonic deadline has elapsed. - Actual before this change: continue polling and enter another five-second sleep. ## Root cause Wall-clock time is adjustable and is not suitable for measuring elapsed durations. ## Fix Use Python's monotonic clock for both the initial timestamp and every deadline check. This does not change the public API, poll interval, terminal states, or error message. ## Validation - `tests/lib/test_file_processing.py`: 15 passed - Ruff check: passed - Ruff format check: passed - Isolated synchronous and asynchronous rollback reproducer: passed Commands used: ```text PYTHONPATH=/tmp/openai-python-file-timeout-deps-20260828:src python -m pytest -p no:cacheprovider -o addopts= --confcutdir=tests/lib --asyncio-mode=auto -q tests/lib/test_file_processing.py python -m ruff check --no-cache src/openai/lib/_files.py tests/lib/test_file_processing.py python -m ruff format --no-cache --check src/openai/lib/_files.py tests/lib/test_file_processing.py ``` --------- Co-authored-by: Marcus Wood <marcuswood@openai.com>
1 parent 841d929 commit 58aca1d

2 files changed

Lines changed: 16 additions & 8 deletions

File tree

‎src/openai/lib/_files.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ def wait_for_file_processing(
1919
"""Poll a file using the caller's resource and sleep hooks."""
2020
TERMINAL_STATES = {"processed", "error", "deleted"}
2121

22-
start = time.time()
22+
start = time.monotonic()
2323
file = files.retrieve(id)
2424
while file.status not in TERMINAL_STATES:
2525
files._sleep(poll_interval)
2626

2727
file = files.retrieve(id)
28-
if time.time() - start > max_wait_seconds:
28+
if time.monotonic() - start > max_wait_seconds:
2929
raise RuntimeError(
3030
f"Giving up on waiting for file {id} to finish processing after {max_wait_seconds} seconds."
3131
)
@@ -43,13 +43,13 @@ async def async_wait_for_file_processing(
4343
"""Poll a file using the caller's async resource and sleep hooks."""
4444
TERMINAL_STATES = {"processed", "error", "deleted"}
4545

46-
start = time.time()
46+
start = time.monotonic()
4747
file = await files.retrieve(id)
4848
while file.status not in TERMINAL_STATES:
4949
await files._sleep(poll_interval)
5050

5151
file = await files.retrieve(id)
52-
if time.time() - start > max_wait_seconds:
52+
if time.monotonic() - start > max_wait_seconds:
5353
raise RuntimeError(
5454
f"Giving up on waiting for file {id} to finish processing after {max_wait_seconds} seconds."
5555
)

‎tests/lib/test_file_processing.py‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,23 @@ async def test_poll_until_processed(files_resource: Files | AsyncFiles) -> None:
5656
sleep.assert_called_once_with(0.25)
5757

5858

59-
async def test_timeout(files_resource: Files | AsyncFiles) -> None:
59+
async def test_timeout_after_wall_clock_rollback(files_resource: Files | AsyncFiles) -> None:
6060
with (
6161
mock.patch.object(file_helpers, "time") as clock,
6262
mock.patch.object(files_resource, "retrieve", return_value=make_file("uploaded")),
63-
mock.patch.object(files_resource, "_sleep"),
63+
mock.patch.object(
64+
files_resource,
65+
"_sleep",
66+
side_effect=[None, AssertionError("poll continued after the deadline")],
67+
) as sleep,
6468
):
65-
clock.time.side_effect = [0.0, 11.0]
69+
clock.time.side_effect = [100.0, 90.0]
70+
clock.monotonic.side_effect = [100.0, 102.0]
71+
6672
with pytest.raises(RuntimeError, match=f"Giving up on waiting for file {FILE_ID}"):
67-
await wait(files_resource, max_wait_seconds=10)
73+
await wait(files_resource, poll_interval=5.0, max_wait_seconds=1.0)
74+
75+
sleep.assert_called_once_with(5.0)
6876

6977

7078
async def test_retrieve_error_propagates(files_resource: Files | AsyncFiles) -> None:

0 commit comments

Comments
 (0)