Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 98888da

Browse files
authored
fix: add method to close httplib2 connections (googleapis#1038)
Fixes googleapis#618 🦕 httplib2 leaves sockets open by default. This can lead to situations where programs run out of available sockets. httplib2 added a method last year to clean up connections. https://github.com/httplib2/httplib2/blob/9bf300cdc372938f4237150d5b9b615879eb51a1/python3/httplib2/__init__.py#L1498-L1506 This PR adds two ways to close http connections. The interface is intentionally similar to the proposed design for GAPIC clients. googleapis/gapic-generator-python#575
1 parent 8289ae9 commit 98888da

File tree

4 files changed

+69
-4
lines changed

4 files changed

+69
-4
lines changed

docs/start.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,24 @@ This section describes how to build an API-specific service object, make calls t
4545

4646
### Build the service object
4747

48-
Whether you are using simple or authorized API access, you use the [build()](http://googleapis.github.io/google-api-python-client/docs/epy/googleapiclient.discovery-module.html#build) function to create a service object. It takes an API name and API version as arguments. You can see the list of all API versions on the [Supported APIs](dyn/index.md) page. The service object is constructed with methods specific to the given API. To create it, do the following:
48+
Whether you are using simple or authorized API access, you use the [build()](http://googleapis.github.io/google-api-python-client/docs/epy/googleapiclient.discovery-module.html#build) function to create a service object. It takes an API name and API version as arguments. You can see the list of all API versions on the [Supported APIs](dyn/index.md) page. The service object is constructed with methods specific to the given API.
49+
50+
`httplib2`, the underlying transport library, makes all connections persistent by default. Use the service object with a context manager or call `close` to avoid leaving sockets open.
4951

5052

5153
```python
5254
from googleapiclient.discovery import build
53-
service = build('api_name', 'api_version', ...)
55+
56+
service = build('drive', 'v3')
57+
# ...
58+
service.close()
59+
```
60+
61+
```python
62+
from googleapiclient.discovery import build
63+
64+
with build('drive', 'v3') as service:
65+
# ...
5466
```
5567

5668
### Collections

googleapiclient/discovery.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,8 @@ def build(
261261
else:
262262
discovery_http = http
263263

264+
service = None
265+
264266
for discovery_url in _discovery_service_uri_options(discoveryServiceUrl, version):
265267
requested_url = uritemplate.expand(discovery_url, params)
266268

@@ -273,7 +275,7 @@ def build(
273275
developerKey,
274276
num_retries=num_retries,
275277
)
276-
return build_from_document(
278+
service = build_from_document(
277279
content,
278280
base=discovery_url,
279281
http=http,
@@ -285,13 +287,22 @@ def build(
285287
adc_cert_path=adc_cert_path,
286288
adc_key_path=adc_key_path,
287289
)
290+
break # exit if a service was created
288291
except HttpError as e:
289292
if e.resp.status == http_client.NOT_FOUND:
290293
continue
291294
else:
292295
raise e
293296

294-
raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))
297+
# If discovery_http was created by this function, we are done with it
298+
# and can safely close it
299+
if http is None:
300+
discovery_http.close()
301+
302+
if service is None:
303+
raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))
304+
else:
305+
return service
295306

296307

297308
def _discovery_service_uri_options(discoveryServiceUrl, version):
@@ -1309,6 +1320,20 @@ def __setstate__(self, state):
13091320
self._dynamic_attrs = []
13101321
self._set_service_methods()
13111322

1323+
1324+
def __enter__(self):
1325+
return self
1326+
1327+
def __exit__(self, exc_type, exc, exc_tb):
1328+
self.close()
1329+
1330+
def close(self):
1331+
"""Close httplib2 connections."""
1332+
# httplib2 leaves sockets open by default.
1333+
# Cleanup using the `close` method.
1334+
# https://github.com/httplib2/httplib2/issues/148
1335+
self._http.http.close()
1336+
13121337
def _set_service_methods(self):
13131338
self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
13141339
self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)

googleapiclient/http.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1720,6 +1720,8 @@ def request(
17201720
self.headers = headers
17211721
return httplib2.Response(self.response_headers), self.data
17221722

1723+
def close(self):
1724+
return None
17231725

17241726
class HttpMockSequence(object):
17251727
"""Mock of httplib2.Http

tests/test_discovery.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,13 @@ def test_ResourceMethodParameters_zoo_animals_patch(self):
437437
self.assertEqual(parameters.enum_params, {})
438438

439439

440+
class Discovery(unittest.TestCase):
441+
def test_discovery_http_is_closed(self):
442+
http = HttpMock(datafile("malformed.json"), {"status": "200"})
443+
service = build("plus", "v1", credentials=mock.sentinel.credentials)
444+
http.close.assert_called_once()
445+
446+
440447
class DiscoveryErrors(unittest.TestCase):
441448
def test_tests_should_be_run_with_strict_positional_enforcement(self):
442449
try:
@@ -572,6 +579,25 @@ def test_building_with_developer_key_skips_adc(self):
572579
# application default credentials were used.
573580
self.assertNotIsInstance(plus._http, google_auth_httplib2.AuthorizedHttp)
574581

582+
def test_building_with_context_manager(self):
583+
discovery = read_datafile("plus.json")
584+
with mock.patch("httplib2.Http") as http:
585+
with build_from_document(discovery, base="https://www.googleapis.com/", credentials=self.MOCK_CREDENTIALS) as plus:
586+
self.assertIsNotNone(plus)
587+
self.assertTrue(hasattr(plus, "activities"))
588+
plus._http.http.close.assert_called_once()
589+
590+
def test_resource_close(self):
591+
discovery = read_datafile("plus.json")
592+
with mock.patch("httplib2.Http") as http:
593+
plus = build_from_document(
594+
discovery,
595+
base="https://www.googleapis.com/",
596+
credentials=self.MOCK_CREDENTIALS,
597+
)
598+
plus.close()
599+
plus._http.http.close.assert_called_once()
600+
575601
def test_api_endpoint_override_from_client_options(self):
576602
discovery = read_datafile("plus.json")
577603
api_endpoint = "https://foo.googleapis.com/"

0 commit comments

Comments
 (0)