From 23371b431778b92742b32fc2fec957f7aab56058 Mon Sep 17 00:00:00 2001 From: HughhhhCoder Date: Fri, 28 Aug 2026 00:08:59 +0800 Subject: [PATCH] fix: use monotonic clock for file processing timeout --- src/openai/lib/_files.py | 8 ++++---- tests/lib/test_file_processing.py | 26 +++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/openai/lib/_files.py b/src/openai/lib/_files.py index ce88c689f2..5b131fdf0c 100644 --- a/src/openai/lib/_files.py +++ b/src/openai/lib/_files.py @@ -19,13 +19,13 @@ def wait_for_file_processing( """Poll a file using the caller's resource and sleep hooks.""" TERMINAL_STATES = {"processed", "error", "deleted"} - start = time.time() + start = time.monotonic() file = files.retrieve(id) while file.status not in TERMINAL_STATES: files._sleep(poll_interval) file = files.retrieve(id) - if time.time() - start > max_wait_seconds: + if time.monotonic() - start > max_wait_seconds: raise RuntimeError( f"Giving up on waiting for file {id} to finish processing after {max_wait_seconds} seconds." ) @@ -43,13 +43,13 @@ async def async_wait_for_file_processing( """Poll a file using the caller's async resource and sleep hooks.""" TERMINAL_STATES = {"processed", "error", "deleted"} - start = time.time() + start = time.monotonic() file = await files.retrieve(id) while file.status not in TERMINAL_STATES: await files._sleep(poll_interval) file = await files.retrieve(id) - if time.time() - start > max_wait_seconds: + if time.monotonic() - start > max_wait_seconds: raise RuntimeError( f"Giving up on waiting for file {id} to finish processing after {max_wait_seconds} seconds." ) diff --git a/tests/lib/test_file_processing.py b/tests/lib/test_file_processing.py index 8ac02acfcc..be589b598b 100644 --- a/tests/lib/test_file_processing.py +++ b/tests/lib/test_file_processing.py @@ -62,11 +62,35 @@ async def test_timeout(files_resource: Files | AsyncFiles) -> None: mock.patch.object(files_resource, "retrieve", return_value=make_file("uploaded")), mock.patch.object(files_resource, "_sleep"), ): - clock.time.side_effect = [0.0, 11.0] + clock.monotonic.side_effect = [0.0, 11.0] with pytest.raises(RuntimeError, match=f"Giving up on waiting for file {FILE_ID}"): await wait(files_resource, max_wait_seconds=10) +async def test_timeout_uses_monotonic_clock_after_wall_clock_rollback( + files_resource: Files | AsyncFiles, +) -> None: + class PollContinued(RuntimeError): + pass + + with ( + mock.patch.object(file_helpers, "time") as clock, + mock.patch.object(files_resource, "retrieve", return_value=make_file("uploaded")), + mock.patch.object( + files_resource, + "_sleep", + side_effect=[None, PollContinued("poll continued after the deadline")], + ) as sleep, + ): + clock.time.side_effect = [100.0, 90.0] + clock.monotonic.side_effect = [100.0, 102.0] + + with pytest.raises(RuntimeError, match=f"Giving up on waiting for file {FILE_ID}"): + await wait(files_resource, poll_interval=5.0, max_wait_seconds=1.0) + + sleep.assert_called_once_with(5.0) + + async def test_retrieve_error_propagates(files_resource: Files | AsyncFiles) -> None: error = RuntimeError("synthetic retrieval failure") with mock.patch.object(files_resource, "retrieve", side_effect=error):