Skip to content

Commit 77e5cf9

Browse files
authored
Periodically close open request.Sessions to avoid buggy interaction with Docker Desktop (#478)
* Periodically refresh open `requests.Session`s to mitigate open filehandle issues (#179) As reported, we create a `requests.Session` object on first request to the servers and then reuse it indefinitely. This can leave some open file handles on the OS (not a big deal), but can interact poorly with a bug in Docker Desktop which causes the SDK to entierly break connections to the server. See #140 for more info. The order of items in the API responses is intentional, and this order is clobbered by the rendering of `OpenAIObject`. This change removes the alphabetic sort of response keys
1 parent fe3abd1 commit 77e5cf9

File tree

4 files changed

+68
-1
lines changed

4 files changed

+68
-1
lines changed

openai/api_requestor.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import platform
44
import sys
55
import threading
6+
import time
67
import warnings
78
from contextlib import asynccontextmanager
89
from json import JSONDecodeError
@@ -32,6 +33,7 @@
3233
from openai.util import ApiType
3334

3435
TIMEOUT_SECS = 600
36+
MAX_SESSION_LIFETIME_SECS = 180
3537
MAX_CONNECTION_RETRIES = 2
3638

3739
# Has one attribute per thread, 'session'.
@@ -516,6 +518,14 @@ def request_raw(
516518

517519
if not hasattr(_thread_context, "session"):
518520
_thread_context.session = _make_session()
521+
_thread_context.session_create_time = time.time()
522+
elif (
523+
time.time() - getattr(_thread_context, "session_create_time", 0)
524+
>= MAX_SESSION_LIFETIME_SECS
525+
):
526+
_thread_context.session.close()
527+
_thread_context.session = _make_session()
528+
_thread_context.session_create_time = time.time()
519529
try:
520530
result = _thread_context.session.request(
521531
method,

openai/openai_object.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ def __repr__(self):
278278

279279
def __str__(self):
280280
obj = self.to_dict_recursive()
281-
return json.dumps(obj, sort_keys=True, indent=2)
281+
return json.dumps(obj, indent=2)
282282

283283
def to_dict(self):
284284
return dict(self)

openai/tests/test_api_requestor.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,35 @@ def test_requestor_azure_ad_headers() -> None:
6767
assert headers["Test_Header"] == "Unit_Test_Header"
6868
assert "Authorization" in headers
6969
assert headers["Authorization"] == "Bearer test_key"
70+
71+
72+
@pytest.mark.requestor
73+
def test_requestor_cycle_sessions(mocker: MockerFixture) -> None:
74+
# HACK: we need to purge the _thread_context to not interfere
75+
# with other tests
76+
from openai.api_requestor import _thread_context
77+
78+
delattr(_thread_context, "session")
79+
80+
api_requestor = APIRequestor(key="test_key", api_type="azure_ad")
81+
82+
mock_session = mocker.MagicMock()
83+
mocker.patch("openai.api_requestor._make_session", lambda: mock_session)
84+
85+
# We don't call `session.close()` if not enough time has elapsed
86+
api_requestor.request_raw("get", "http://example.com")
87+
mock_session.request.assert_called()
88+
api_requestor.request_raw("get", "http://example.com")
89+
mock_session.close.assert_not_called()
90+
91+
mocker.patch("openai.api_requestor.MAX_SESSION_LIFETIME_SECS", 0)
92+
93+
# Due to 0 lifetime, the original session will be closed before the next call
94+
# and a new session will be created
95+
mock_session_2 = mocker.MagicMock()
96+
mocker.patch("openai.api_requestor._make_session", lambda: mock_session_2)
97+
api_requestor.request_raw("get", "http://example.com")
98+
mock_session.close.assert_called()
99+
mock_session_2.request.assert_called()
100+
101+
delattr(_thread_context, "session")

openai/tests/test_util.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from tempfile import NamedTemporaryFile
23

34
import pytest
@@ -28,3 +29,27 @@ def test_openai_api_key_path_with_malformed_key(api_key_file) -> None:
2829
api_key_file.flush()
2930
with pytest.raises(ValueError, match="Malformed API key"):
3031
util.default_api_key()
32+
33+
34+
def test_key_order_openai_object_rendering() -> None:
35+
sample_response = {
36+
"id": "chatcmpl-7NaPEA6sgX7LnNPyKPbRlsyqLbr5V",
37+
"object": "chat.completion",
38+
"created": 1685855844,
39+
"model": "gpt-3.5-turbo-0301",
40+
"usage": {"prompt_tokens": 57, "completion_tokens": 40, "total_tokens": 97},
41+
"choices": [
42+
{
43+
"message": {
44+
"role": "assistant",
45+
"content": "The 2020 World Series was played at Globe Life Field in Arlington, Texas. It was the first time that the World Series was played at a neutral site because of the COVID-19 pandemic.",
46+
},
47+
"finish_reason": "stop",
48+
"index": 0,
49+
}
50+
],
51+
}
52+
53+
oai_object = util.convert_to_openai_object(sample_response)
54+
# The `__str__` method was sorting while dumping to json
55+
assert list(json.loads(str(oai_object)).keys()) == list(sample_response.keys())

0 commit comments

Comments
 (0)