Skip to content

Commit 34e6829

Browse files
Lekensteynmilas
andauthored
exec: fix file handle leak with container.exec_* APIs (#2320)
Requests with stream=True MUST be closed or else the connection will never be returned to the connection pool. Both ContainerApiMixin.attach and ExecApiMixin.exec_start were leaking in the stream=False case. exec_start was modified to follow attach for the stream=True case as that allows the caller to close the stream when done (untested). Tested with: # Test exec_run (stream=False) - observe one less leak make integration-test-py3 file=models_containers_test.py' -k test_exec_run_success -vs -W error::ResourceWarning' # Test exec_start (stream=True, fully reads from CancellableStream) make integration-test-py3 file=api_exec_test.py' -k test_execute_command -vs -W error::ResourceWarning' After this change, one resource leak is removed, the remaining resource leaks occur because none of the tests call client.close(). Fixes #1293 (Regression from #1130) Signed-off-by: Peter Wu <pwu@cloudflare.com> Co-authored-by: Milas Bowman <milas.bowman@docker.com>
1 parent 22718ba commit 34e6829

File tree

2 files changed

+23
-6
lines changed

2 files changed

+23
-6
lines changed

docker/api/client.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,10 @@ def _stream_raw_result(self, response, chunk_size=1, decode=True):
406406
yield from response.iter_content(chunk_size, decode)
407407

408408
def _read_from_socket(self, response, stream, tty=True, demux=False):
409+
"""Consume all data from the socket, close the response and return the
410+
data. If stream=True, then a generator is returned instead and the
411+
caller is responsible for closing the response.
412+
"""
409413
socket = self._get_raw_response_socket(response)
410414

411415
gen = frames_iter(socket, tty)
@@ -420,8 +424,11 @@ def _read_from_socket(self, response, stream, tty=True, demux=False):
420424
if stream:
421425
return gen
422426
else:
423-
# Wait for all the frames, concatenate them, and return the result
424-
return consume_socket_output(gen, demux=demux)
427+
try:
428+
# Wait for all frames, concatenate them, and return the result
429+
return consume_socket_output(gen, demux=demux)
430+
finally:
431+
response.close()
425432

426433
def _disable_socket_timeout(self, socket):
427434
""" Depending on the combination of python version and whether we're

docker/api/exec_api.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .. import errors
22
from .. import utils
3+
from ..types import CancellableStream
34

45

56
class ExecApiMixin:
@@ -125,9 +126,10 @@ def exec_start(self, exec_id, detach=False, tty=False, stream=False,
125126
detach (bool): If true, detach from the exec command.
126127
Default: False
127128
tty (bool): Allocate a pseudo-TTY. Default: False
128-
stream (bool): Stream response data. Default: False
129+
stream (bool): Return response data progressively as an iterator
130+
of strings, rather than a single string.
129131
socket (bool): Return the connection socket to allow custom
130-
read/write operations.
132+
read/write operations. Must be closed by the caller when done.
131133
demux (bool): Return stdout and stderr separately
132134
133135
Returns:
@@ -161,7 +163,15 @@ def exec_start(self, exec_id, detach=False, tty=False, stream=False,
161163
stream=True
162164
)
163165
if detach:
164-
return self._result(res)
166+
try:
167+
return self._result(res)
168+
finally:
169+
res.close()
165170
if socket:
166171
return self._get_raw_response_socket(res)
167-
return self._read_from_socket(res, stream, tty=tty, demux=demux)
172+
173+
output = self._read_from_socket(res, stream, tty=tty, demux=demux)
174+
if stream:
175+
return CancellableStream(output, res)
176+
else:
177+
return output

0 commit comments

Comments
 (0)