@@ -108,6 +108,11 @@ def _path_importer_cache(cls, path):
108108except NameError :
109109 BaseException = Exception
110110
111+ if sys .version_info >= (2 , 6 ):
112+ from io import BlockingIOError
113+ else :
114+ BlockingIOError = None
115+
111116try :
112117 ModuleNotFoundError
113118except 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
0 commit comments