Skip to content

Commit a44dfb5

Browse files
committed
Fix Fn API data plane deadlock when outbound queue is full
1 parent 290e372 commit a44dfb5

2 files changed

Lines changed: 51 additions & 10 deletions

File tree

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
{
22
"comment": "Modify this file in a trivial way to cause this test suite to run",
3-
"revision": 1
4-
}
3+
"revision": 3

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

Lines changed: 50 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,
478+
maxbytes=_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,38 @@ 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()
482497
self._closed = True
483498

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

596638
def close_callback(data):
597639
# type: (bytes) -> None
@@ -601,7 +643,7 @@ def close_callback(data):
601643
instruction_id=instruction_id,
602644
transform_id=transform_id,
603645
is_last=True)
604-
self._to_send.put(elem, self._get_element_size_bytes(elem))
646+
self._enqueue_to_send(elem)
605647

606648
return ClosableOutputStream.create(
607649
close_callback, add_to_send_queue, self._data_buffer_time_limit_ms)
@@ -622,7 +664,7 @@ def add_to_send_queue(timer):
622664
timer_family_id=timer_family_id,
623665
timers=timer,
624666
is_last=False)
625-
self._to_send.put(elem, self._get_element_size_bytes(elem))
667+
self._enqueue_to_send(elem)
626668

627669
def close_callback(timer):
628670
# type: (bytes) -> None
@@ -632,7 +674,7 @@ def close_callback(timer):
632674
transform_id=transform_id,
633675
timer_family_id=timer_family_id,
634676
is_last=True)
635-
self._to_send.put(elem, self._get_element_size_bytes(elem))
677+
self._enqueue_to_send(elem)
636678

637679
return ClosableOutputStream.create(
638680
close_callback, add_to_send_queue, self._data_buffer_time_limit_ms)

0 commit comments

Comments
 (0)