@@ -65,6 +65,20 @@ def _init_timeout(timeout=CONNECTION_TIMEOUT):
6565def _check_timeout (t ):
6666 return time .monotonic () > t
6767
68+
69+ class _SentinelReady (Exception ):
70+ """A wait sentinel became readable while a connection read was in progress.
71+
72+ Used by ProcessPoolExecutor so a worker killed mid-result does not leave
73+ the manager thread blocked in a read that can never see EOF. The parent's
74+ copy of the result pipe's write end stays open for the executor's
75+ lifetime, so the death of the writer is not delivered as end-of-file.
76+ """
77+
78+ def __init__ (self , ready ):
79+ super ().__init__ (ready )
80+ self .ready = ready
81+
6882#
6983#
7084#
@@ -253,11 +267,18 @@ def recv_bytes_into(self, buf, offset=0):
253267 (offset + size ) // itemsize ])
254268 return size
255269
256- def recv (self ):
257- """Receive a (picklable) object"""
270+ def recv (self , * , sentinels = ()):
271+ """Receive a (picklable) object.
272+
273+ *sentinels* is an optional sequence of objects accepted by
274+ :func:`wait`, such as process sentinels. Before each read, those
275+ objects are waited on together with this connection. If one of them
276+ is ready and this connection is not, :class:`_SentinelReady` is
277+ raised instead of blocking until the rest of the message arrives.
278+ """
258279 self ._check_closed ()
259280 self ._check_readable ()
260- buf = self ._recv_bytes ()
281+ buf = self ._recv_bytes (sentinels = sentinels )
261282 return _ForkingPickler .loads (buf .getbuffer ())
262283
263284 def poll (self , timeout = 0.0 ):
@@ -317,7 +338,31 @@ def _send_bytes(self, buf):
317338 assert err == 0
318339 assert nwritten == len (buf )
319340
320- def _recv_bytes (self , maxsize = None ):
341+ def _wait_overlapped_or_sentinels (self , ov , sentinels ):
342+ """Block until *ov* completes, or a sentinel is signaled."""
343+ if not sentinels :
344+ waitres = _winapi .WaitForMultipleObjects (
345+ [ov .event ], False , INFINITE )
346+ assert waitres == WAIT_OBJECT_0
347+ return
348+ sentinels = list (sentinels )
349+ handles = [ov .event , * (int (item ) for item in sentinels )]
350+ waitres = _winapi .WaitForMultipleObjects (handles , False , INFINITE )
351+ if waitres == WAIT_OBJECT_0 :
352+ return
353+ ov .cancel ()
354+ try :
355+ ov .GetOverlappedResult (True )
356+ except OSError :
357+ pass
358+ idx = waitres - WAIT_OBJECT_0
359+ if not 0 < idx <= len (sentinels ):
360+ idx = waitres - WAIT_ABANDONED_0
361+ if 0 < idx <= len (sentinels ):
362+ raise _SentinelReady ([sentinels [idx - 1 ]])
363+ raise OSError ("failed waiting for pipe data" )
364+
365+ def _recv_bytes (self , maxsize = None , sentinels = ()):
321366 if self ._got_empty_message :
322367 self ._got_empty_message = False
323368 return io .BytesIO ()
@@ -329,23 +374,29 @@ def _recv_bytes(self, maxsize=None):
329374
330375 sentinel = object ()
331376 return_value = sentinel
377+ sentinel_ready = False
332378 try :
333379 try :
334380 if err == _winapi .ERROR_IO_PENDING :
335- waitres = _winapi .WaitForMultipleObjects (
336- [ov .event ], False , INFINITE )
337- assert waitres == WAIT_OBJECT_0
381+ self ._wait_overlapped_or_sentinels (
382+ ov , sentinels )
383+ except _SentinelReady :
384+ # Already cancelled and reaped above.
385+ sentinel_ready = True
386+ raise
338387 except :
339388 ov .cancel ()
340389 raise
341390 finally :
342- nread , err = ov .GetOverlappedResult (True )
343- if err == 0 :
344- f = io .BytesIO ()
345- f .write (ov .getbuffer ())
346- return_value = f
347- elif err == _winapi .ERROR_MORE_DATA :
348- return_value = self ._get_more_data (ov , maxsize )
391+ if not sentinel_ready :
392+ nread , err = ov .GetOverlappedResult (True )
393+ if err == 0 :
394+ f = io .BytesIO ()
395+ f .write (ov .getbuffer ())
396+ return_value = f
397+ elif err == _winapi .ERROR_MORE_DATA :
398+ return_value = self ._get_more_data (
399+ ov , maxsize , sentinels )
349400 except :
350401 if return_value is sentinel :
351402 raise
@@ -365,11 +416,28 @@ def _poll(self, timeout):
365416 return True
366417 return bool (wait ([self ], timeout ))
367418
368- def _get_more_data (self , ov , maxsize ):
419+ def _get_more_data (self , ov , maxsize , sentinels = () ):
369420 buf = ov .getbuffer ()
370421 f = io .BytesIO ()
371422 f .write (buf )
372423 left = _winapi .PeekNamedPipe (self ._handle )[1 ]
424+ while left == 0 and sentinels :
425+ # The rest of this message has not been written yet. The
426+ # parent keeps its own write end open, so a killed writer
427+ # does not surface as EOF. Poll the sentinels directly:
428+ # waiting on this pipe here would start another overlapped
429+ # read and consume the message.
430+ handles = [int (item ) for item in sentinels ]
431+ waitres = _winapi .WaitForMultipleObjects (handles , False , 50 )
432+ if waitres == WAIT_TIMEOUT :
433+ left = _winapi .PeekNamedPipe (self ._handle )[1 ]
434+ continue
435+ idx = waitres - WAIT_OBJECT_0
436+ if not 0 <= idx < len (handles ):
437+ idx = waitres - WAIT_ABANDONED_0
438+ if 0 <= idx < len (handles ):
439+ raise _SentinelReady ([sentinels [idx ]])
440+ raise OSError ("failed waiting for pipe data" )
373441 assert left > 0
374442 if maxsize is not None and len (buf ) + left > maxsize :
375443 self ._bad_message_length ()
@@ -407,11 +475,27 @@ def _send(self, buf, write=_write):
407475 break
408476 buf = buf [n :]
409477
410- def _recv (self , size , read = _read ):
478+ def _abandon_read_if_sentinel_ready (self , sentinels ):
479+ """Raise if a sentinel is ready and this connection has no data.
480+
481+ A non-blocking poll covers the common case where the next chunk is
482+ already buffered, so a large result does not block once per chunk.
483+ """
484+ ready = wait ([self , * sentinels ], timeout = 0 )
485+ if self in ready :
486+ return
487+ if not ready :
488+ ready = wait ([self , * sentinels ])
489+ if self not in ready :
490+ raise _SentinelReady (ready )
491+
492+ def _recv (self , size , sentinels = (), read = _read ):
411493 buf = io .BytesIO ()
412494 handle = self ._handle
413495 remaining = size
414496 while remaining > 0 :
497+ if sentinels :
498+ self ._abandon_read_if_sentinel_ready (sentinels )
415499 to_read = min (BUFSIZE , remaining )
416500 chunk = read (handle , to_read )
417501 n = len (chunk )
@@ -447,15 +531,15 @@ def _send_bytes(self, buf):
447531 # to avoid "broken pipe" errors if the other end closed the pipe.
448532 self ._send (header + buf )
449533
450- def _recv_bytes (self , maxsize = None ):
451- buf = self ._recv (4 )
534+ def _recv_bytes (self , maxsize = None , sentinels = () ):
535+ buf = self ._recv (4 , sentinels )
452536 size , = struct .unpack ("!i" , buf .getvalue ())
453537 if size == - 1 :
454- buf = self ._recv (8 )
538+ buf = self ._recv (8 , sentinels )
455539 size , = struct .unpack ("!Q" , buf .getvalue ())
456540 if maxsize is not None and size > maxsize :
457541 return None
458- return self ._recv (size )
542+ return self ._recv (size , sentinels )
459543
460544 def _poll (self , timeout ):
461545 r = wait ([self ], timeout )
0 commit comments