Skip to content

Commit 1b261f0

Browse files
authored
Fix Fn API data plane deadlock when outbound queue is full (#38581)
1 parent 1858d2e commit 1b261f0

1 file changed

Lines changed: 52 additions & 8 deletions

File tree

sdks/python/apache_beam/runners/worker/data_plane.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@
6868
# Keep a set of completed instructions to discard late received data. The set
6969
# can have up to _MAX_CLEANED_INSTRUCTIONS items. See _GrpcDataChannel.
7070
_MAX_CLEANED_INSTRUCTIONS = 10000
71+
_DEFAULT_SEND_QUEUE_MAX_ELEMENTS = 10000
72+
_DEFAULT_SEND_QUEUE_MAX_BYTES = 100 << 20 # 100MB
73+
_DEFAULT_RECEIVE_QUEUE_MAX_ELEMENTS = 5
7174

7275
# retry on transient UNAVAILABLE grpc error from data channels.
7376
_GRPC_SERVICE_CONFIG = json.dumps({
@@ -459,10 +462,20 @@ def __init__(self, data_buffer_time_limit_ms=0):
459462

460463
self._data_buffer_time_limit_ms = data_buffer_time_limit_ms
461464
self._to_send = ByteLimitedQueue(
462-
maxsize=10000,
463-
maxbytes=100 << 20) # type: ByteLimitedQueue[DataOrTimers]
465+
maxsize=_DEFAULT_SEND_QUEUE_MAX_ELEMENTS,
466+
maxbytes=_DEFAULT_SEND_QUEUE_MAX_BYTES
467+
) # type: ByteLimitedQueue[DataOrTimers]
468+
# Staging queue so a full send buffer does not block reading inputs.
469+
self._pending_send = ByteLimitedQueue(
470+
maxsize=_DEFAULT_SEND_QUEUE_MAX_ELEMENTS,
471+
maxbytes=_DEFAULT_SEND_QUEUE_MAX_BYTES
472+
) # type: ByteLimitedQueue[DataOrTimers]
473+
self._send_forwarder = None # type: Optional[threading.Thread]
474+
self._start_send_forwarder()
464475
self._received = collections.defaultdict(
465-
lambda: ByteLimitedQueue(maxsize=5, maxbytes=100 << 20)
476+
lambda: ByteLimitedQueue(
477+
maxsize=_DEFAULT_RECEIVE_QUEUE_MAX_ELEMENTS, maxbytes=
478+
_DEFAULT_SEND_QUEUE_MAX_BYTES)
466479
) # type: DefaultDict[str, ByteLimitedQueue[DataOrTimers]]
467480

468481
# Keep a cache of completed instructions. Data for completed instructions
@@ -478,9 +491,40 @@ def __init__(self, data_buffer_time_limit_ms=0):
478491

479492
def close(self):
480493
# type: () -> None
481-
self._to_send.put(self._WRITES_FINISHED, 0)
494+
self._enqueue_to_send(self._WRITES_FINISHED)
495+
if self._send_forwarder is not None:
496+
self._send_forwarder.join()
497+
if self._exception:
498+
raise self._exception
482499
self._closed = True
483500

501+
def _start_send_forwarder(self):
502+
# type: () -> None
503+
forwarder = threading.Thread(
504+
target=self._forward_pending_to_send, name='forward_grpc_outputs')
505+
forwarder.daemon = True
506+
forwarder.start()
507+
self._send_forwarder = forwarder
508+
509+
def _enqueue_to_send(self, elem):
510+
# type: (DataOrTimers) -> None
511+
size = self._get_element_size_bytes(elem)
512+
self._pending_send.put((elem, size), size)
513+
514+
def _forward_pending_to_send(self):
515+
# type: () -> None
516+
try:
517+
while True:
518+
elem, size = self._pending_send.get()
519+
self._to_send.put(elem, size)
520+
if elem is self._WRITES_FINISHED:
521+
return
522+
except Exception as e:
523+
if not self._closed:
524+
_LOGGER.exception('Failed to forward outputs in the data plane.')
525+
self._exception = e
526+
raise
527+
484528
def wait(self, timeout=None):
485529
# type: (Optional[int]) -> None
486530
self._reads_finished.wait(timeout)
@@ -591,7 +635,7 @@ def add_to_send_queue(data):
591635
if data:
592636
elem = beam_fn_api_pb2.Elements.Data(
593637
instruction_id=instruction_id, transform_id=transform_id, data=data)
594-
self._to_send.put(elem, self._get_element_size_bytes(elem))
638+
self._enqueue_to_send(elem)
595639

596640
def close_callback(data):
597641
# type: (bytes) -> None
@@ -601,7 +645,7 @@ def close_callback(data):
601645
instruction_id=instruction_id,
602646
transform_id=transform_id,
603647
is_last=True)
604-
self._to_send.put(elem, self._get_element_size_bytes(elem))
648+
self._enqueue_to_send(elem)
605649

606650
return ClosableOutputStream.create(
607651
close_callback, add_to_send_queue, self._data_buffer_time_limit_ms)
@@ -622,7 +666,7 @@ def add_to_send_queue(timer):
622666
timer_family_id=timer_family_id,
623667
timers=timer,
624668
is_last=False)
625-
self._to_send.put(elem, self._get_element_size_bytes(elem))
669+
self._enqueue_to_send(elem)
626670

627671
def close_callback(timer):
628672
# type: (bytes) -> None
@@ -632,7 +676,7 @@ def close_callback(timer):
632676
transform_id=transform_id,
633677
timer_family_id=timer_family_id,
634678
is_last=True)
635-
self._to_send.put(elem, self._get_element_size_bytes(elem))
679+
self._enqueue_to_send(elem)
636680

637681
return ClosableOutputStream.create(
638682
close_callback, add_to_send_queue, self._data_buffer_time_limit_ms)

0 commit comments

Comments
 (0)