Skip to content

Commit 5a8b74e

Browse files
authored
Refactor socket communication handling (#79)
* Refactor socket communication handling Introduce a `contextmanager` to streamline socket attachment and closing. Extract socket processing into a new helper function to improve readability and maintainability of the `docker_communicate` function. * Update ruff linter command in auto-format workflow Changed the ruff linter command from 'ruff' to 'ruff check' in the GitHub Actions auto-format workflow. This aligns the command usage with best practices and ensures consistency in the linting process. --------- Co-authored-by: meanmail <[email protected]>
1 parent 324cd02 commit 5a8b74e

File tree

3 files changed

+85
-49
lines changed

3 files changed

+85
-49
lines changed

.github/workflows/auto-format.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
run: poetry run ruff format
3535

3636
- name: Check files using the ruff linter
37-
run: poetry run ruff --fix --unsafe-fixes --preview --exit-zero .
37+
run: poetry run ruff check --fix --unsafe-fixes --preview --exit-zero .
3838

3939
- name: Commit changes
4040
uses: EndBug/add-and-commit@v9

epicbox/sandboxes.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def create(
9898
raise ValueError(msg)
9999

100100
if not isinstance(workdir, WorkingDirectory | None):
101-
msg = (
101+
msg = ( # type: ignore[unreachable]
102102
"Invalid 'workdir', "
103103
"it should be created using 'working_directory' context manager"
104104
)

epicbox/utils.py

+83-47
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import socket
88
import struct
99
import time
10+
from contextlib import contextmanager
1011
from typing import Any, TYPE_CHECKING
1112

1213
import dateutil.parser
@@ -22,6 +23,8 @@
2223
from epicbox import config, exceptions
2324

2425
if TYPE_CHECKING:
26+
from collections.abc import Iterator
27+
2528
from docker.models.containers import Container
2629

2730
logger = structlog.get_logger()
@@ -157,6 +160,54 @@ def _socket_write(sock: socket.SocketIO, data: bytes) -> int:
157160
raise
158161

159162

163+
def process_sock(
164+
sock: socket.SocketIO, stdin: bytes | None, log: structlog.BoundLogger
165+
) -> tuple[bytes, int]:
166+
"""Process the socket IO.
167+
168+
Read data from the socket if it is ready for reading.
169+
Write data to the socket if it is ready for writing.
170+
:returns: A tuple containing the received data and the number of bytes written.
171+
"""
172+
ready_to_read, ready_to_write, _ = select.select([sock], [sock], [], 1)
173+
received_data: bytes = b""
174+
bytes_written = 0
175+
if ready_to_read:
176+
data = _socket_read(sock)
177+
if data is None:
178+
msg = "Received EOF from the container"
179+
raise EOFError(msg)
180+
received_data = data
181+
182+
if ready_to_write and stdin:
183+
bytes_written = _socket_write(sock, stdin)
184+
if bytes_written >= len(stdin):
185+
log.debug(
186+
"All input data has been sent. "
187+
"Shut down the write half of the socket.",
188+
)
189+
sock._sock.shutdown(socket.SHUT_WR) # type: ignore[attr-defined]
190+
191+
if not ready_to_read and (not ready_to_write or not stdin):
192+
# Save CPU time by sleeping when there is no IO activity.
193+
time.sleep(0.05)
194+
195+
return received_data, bytes_written
196+
197+
198+
@contextmanager
199+
def attach_socket(
200+
docker_client: DockerClient,
201+
container: Container,
202+
params: dict[str, Any],
203+
) -> Iterator[socket.SocketIO]:
204+
sock = docker_client.api.attach_socket(container.id, params=params)
205+
206+
yield sock
207+
208+
sock.close()
209+
210+
160211
def docker_communicate(
161212
container: Container,
162213
stdin: bytes | None = None,
@@ -196,44 +247,33 @@ def docker_communicate(
196247
"stream": 1,
197248
"logs": 0,
198249
}
199-
sock = docker_client.api.attach_socket(container.id, params=params)
200-
sock._sock.setblocking(False) # Make socket non-blocking
201-
log.info(
202-
"Attached to the container",
203-
params=params,
204-
fd=sock.fileno(),
205-
timeout=timeout,
206-
)
207-
if not stdin:
208-
log.debug("There is no input data. Shut down the write half of the socket.")
209-
sock._sock.shutdown(socket.SHUT_WR)
210-
if start_container:
211-
container.start()
212-
log.info("Container started")
213-
214-
stream_data = b""
215-
start_time = time.monotonic()
216-
while timeout is None or time.monotonic() - start_time < timeout:
217-
read_ready, write_ready, _ = select.select([sock], [sock], [], 1)
218-
is_io_active = bool(read_ready or (write_ready and stdin))
219-
220-
if read_ready:
250+
251+
with attach_socket(docker_client, container, params) as sock:
252+
sock._sock.setblocking(False) # type: ignore[attr-defined] # Make socket non-blocking
253+
log.info(
254+
"Attached to the container",
255+
params=params,
256+
fd=sock.fileno(),
257+
timeout=timeout,
258+
)
259+
if not stdin:
260+
log.debug("There is no input data. Shut down the write half of the socket.")
261+
sock._sock.shutdown(socket.SHUT_WR) # type: ignore[attr-defined]
262+
if start_container:
263+
container.start()
264+
log.info("Container started")
265+
266+
stream_data = b""
267+
start_time = time.monotonic()
268+
while timeout is None or time.monotonic() - start_time < timeout:
221269
try:
222-
data = _socket_read(sock)
270+
received_data, bytes_written = process_sock(sock, stdin, log)
223271
except ConnectionResetError:
224272
log.warning(
225273
"Connection reset caught on reading the container "
226274
"output stream. Break communication",
227275
)
228276
break
229-
if data is None:
230-
log.debug("Container output reached EOF. Closing the socket")
231-
break
232-
stream_data += data
233-
234-
if write_ready and stdin:
235-
try:
236-
written = _socket_write(sock, stdin)
237277
except BrokenPipeError:
238278
# Broken pipe may happen when a container terminates quickly
239279
# (e.g. OOM Killer) and docker manages to close the socket
@@ -242,22 +282,18 @@ def docker_communicate(
242282
"Broken pipe caught on writing to stdin. Break communication",
243283
)
244284
break
245-
stdin = stdin[written:]
246-
if not stdin:
247-
log.debug(
248-
"All input data has been sent. Shut down the write "
249-
"half of the socket.",
250-
)
251-
sock._sock.shutdown(socket.SHUT_WR)
252-
253-
if not is_io_active:
254-
# Save CPU time
255-
time.sleep(0.05)
256-
else:
257-
sock.close()
258-
msg = "Container didn't terminate after timeout seconds"
259-
raise TimeoutError(msg)
260-
sock.close()
285+
except EOFError:
286+
log.debug("Container output reached EOF. Closing the socket")
287+
break
288+
289+
if received_data:
290+
stream_data += received_data
291+
if stdin and bytes_written > 0:
292+
stdin = stdin[bytes_written:]
293+
else:
294+
msg = "Container didn't terminate after timeout seconds"
295+
raise TimeoutError(msg)
296+
261297
return demultiplex_docker_stream(stream_data)
262298

263299

0 commit comments

Comments
 (0)