Skip to content

Commit 00ae0b6

Browse files
committed
WIP
1 parent d240a78 commit 00ae0b6

File tree

3 files changed

+134
-24
lines changed

3 files changed

+134
-24
lines changed

mitogen/core.py

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ def _path_importer_cache(cls, path):
108108
except NameError:
109109
BaseException = Exception
110110

111+
if sys.version_info >= (2, 6):
112+
from io import BlockingIOError
113+
else:
114+
BlockingIOError = None
115+
111116
try:
112117
ModuleNotFoundError
113118
except NameError:
@@ -585,29 +590,63 @@ def io_op(func, *args):
585590
or :class:`OSError`, trapping UNIX error codes relating to disconnection
586591
and retry events in various subsystems:
587592
588-
* When a signal is delivered to the process on Python 2, system call retry
589-
is signalled through :data:`errno.EINTR`. The invocation is automatically
590-
restarted.
591-
* When performing IO against a TTY, disconnection of the remote end is
592-
signalled by :data:`errno.EIO`.
593-
* When performing IO against a socket, disconnection of the remote end is
594-
signalled by :data:`errno.ECONNRESET`.
595-
* When performing IO against a pipe, disconnection of the remote end is
596-
signalled by :data:`errno.EPIPE`.
597-
593+
:data:`errno.EINTR`
594+
A system call was interrupted by a signal being delivered.
595+
Python >= 3.5 retries most calls, forever (see PEP 475).
596+
This wrapper retries all calls, also forever.
597+
598+
:exc:`BlockingIOError`
599+
:data:`errno.EAGAIN`
600+
:data:`errno.EWOULDBLOCK`
601+
A system call on a non-blocking file would have blocked.
602+
Python doesn't retry calls. It raises an exception or returns
603+
:data:`None` - depending on the the call, the file, and the version.
604+
This wrapper tries upto ``max_attempts`` times.
605+
606+
:data:`errno.EIO`
607+
:data:`errno.ECONNRESET`
608+
:data:`errno.EPIPE`
609+
IO on a TTY, socket, or pipe respectively disconnected at the other end.
610+
611+
:param func:
612+
The callable to run (e.g. :func:`os.read`, :func:`os.write`).
613+
:param *args:
614+
Positional arguments for the callable.
598615
:returns:
599616
Tuple of `(return_value, disconnect_reason)`, where `return_value` is
600617
the return value of `func(*args)`, and `disconnected` is an exception
601618
instance when disconnection was detected, otherwise :data:`None`.
602619
"""
620+
max_attempts = 5
621+
attempt = 0
603622
while True:
623+
attempt += 1
604624
try:
605625
return func(*args), None
626+
except BlockingIOError:
627+
e = sys.exc_info()[1]
628+
_vv and IOLOG.debug(
629+
'io_op(%r) attempt %d/%d -> %r', func, attempt, max_attempts, e
630+
)
631+
try:
632+
written = e.characters_written
633+
except AttributeError:
634+
written = None
635+
if written:
636+
return written, None
637+
if attempt < max_attempts:
638+
continue
639+
raise
606640
except (select.error, OSError, IOError):
607641
e = sys.exc_info()[1]
608-
_vv and IOLOG.debug('io_op(%r) -> OSError: %s', func, e)
642+
_vv and IOLOG.debug(
643+
'io_op(%r) attempt %d/%d -> %r', func, attempt, max_attempts, e
644+
)
609645
if e.args[0] == errno.EINTR:
610646
continue
647+
if e.args[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
648+
if attempt < max_attempts:
649+
continue
611650
if e.args[0] in (errno.EIO, errno.ECONNRESET, errno.EPIPE):
612651
return None, e
613652
raise
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'''
2+
Write lines of deterministic ASCII test data
3+
4+
```console
5+
$ python generate_ascii_test_data.py 192
6+
000000000000000000 mitogen-test-file ABCDEFGHIJKLMNOPQRSTUVWXYZ
7+
000000000000000064 mitogen-test-file BCDEFGHIJKLMNOPQRSTUVWXYZA
8+
000000000000000128 mitogen-test-file CDEFGHIJKLMNOPQRSTUVWXYZAB
9+
```
10+
'''
11+
12+
import os
13+
import sys
14+
15+
if sys.version_info < (3, 0):
16+
range = xrange # noqa: F821
17+
18+
# Padding added to make each line LINE_SIZE bytes long, including a newline.
19+
# PADDING_POOL is repeated to eliminate repeated concatenations in the loop.
20+
PADDING_TEXT = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.encode('ascii')
21+
PADDING_SIZE = 26
22+
PADDING_POOL = PADDING_TEXT * 2
23+
24+
LINE_TMPL = '%%018d mitogen-test-file %%.%ds\n'.encode('ascii') % PADDING_SIZE
25+
LINE_SIZE = 64
26+
27+
def format_line(lineno):
28+
# type: (int) -> bytes
29+
line_offset = lineno * LINE_SIZE
30+
padding_shift = lineno % PADDING_SIZE
31+
return LINE_TMPL % (line_offset, PADDING_POOL[padding_shift:])
32+
33+
34+
def main():
35+
assert len(PADDING_POOL) >= 2 * PADDING_SIZE
36+
assert len(format_line(0)) == LINE_SIZE
37+
38+
try:
39+
output_size = int(sys.argv[1])
40+
if output_size < 0 or output_size % LINE_SIZE != 0:
41+
raise ValueError
42+
except IndexError:
43+
prog = os.path.basename(sys.argv[0])
44+
raise SystemExit('Usage: %s output_size [output_file]' % prog)
45+
except ValueError:
46+
raise SystemExit(
47+
'Error: output_size must be >= 0 and a multiple of line size (%d), '
48+
'got: %s' % (LINE_SIZE, sys.argv[1])
49+
)
50+
51+
if len(sys.argv) >= 3 and sys.argv[2] != '-':
52+
output_file = open(sys.argv[2], 'wb')
53+
else:
54+
output_file = os.fdopen(sys.stdout.fileno(), 'wb')
55+
56+
with output_file as f:
57+
for lineno in range(0, output_size // LINE_SIZE):
58+
line = format_line(lineno)
59+
f.write(line)
60+
61+
raise SystemExit
62+
63+
64+
if __name__ == '__main__':
65+
main()

tests/ansible/regression/issue_615__streaming_transfer.yml

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,36 @@
77
become: true
88
vars:
99
mitogen_ssh_compression: false
10+
file_path_controller: "/tmp/fetch-{{ inventory_hostname }}-512mb.txt"
11+
file_path_target: /tmp/mitogen-test-512mb.txt
12+
file_size: "{{ 512 * 2**20 }}"
1013
tasks:
1114
- include_tasks: _mitogen_only.yml
1215
- block:
13-
- name: Create /tmp/512mb.zero
14-
shell: |
15-
dd if=/dev/zero of=/tmp/512mb.zero bs=1048576 count=512;
16-
chmod go= /tmp/512mb.zero
17-
args:
18-
creates: /tmp/512mb.zero
16+
- name: Create test file on target
17+
script:
18+
cmd: generate_ascii_test_data.py "{{ file_size }}" "{{ file_path_target }}"
19+
executable: "{{ ansible_python_interpreter | default(ansible_facts.discovered_interpreter_python) }}"
20+
creates: "{{ file_path_target }}"
21+
register: target_file_task
1922

20-
- name: Fetch /tmp/512mb.zero
23+
- debug:
24+
var: target_file_task
25+
26+
- name: Fetch test file
2127
fetch:
22-
src: /tmp/512mb.zero
23-
dest: /tmp/fetch-{{ inventory_hostname }}-512mb.zero
28+
src: "{{ file_path_target }}"
29+
dest: "{{ file_path_controller }}"
2430
flat: true
2531

26-
- name: Cleanup /tmp/512mb.zero
32+
- name: Cleanup target
2733
file:
28-
path: /tmp/512mb.zero
34+
path: "{{ file_path_target }}"
2935
state: absent
3036

31-
- name: Cleanup fetched file
37+
- name: Cleanup controller
3238
file:
33-
path: /tmp/fetch-{{ inventory_hostname }}-512mb.zero
39+
path: "{{ file_path_controller }}"
3440
state: absent
3541
become: false
3642
delegate_to: localhost

0 commit comments

Comments
 (0)