Skip to content

Commit 581ec8b

Browse files
authored
Always mark the instruction as cleaned up in the GRPC data channel when processing an instruction fails. (#36367)
* Mark instructions as cleaned up in the GRPC data channel if processing an instruction fails. * Invoke cleanup even if BP failed to create. * Address feedback. * Add a test
1 parent f07ccf3 commit 581ec8b

3 files changed

Lines changed: 65 additions & 11 deletions

File tree

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,11 @@ def _clean_receiving_queue(self, instruction_id):
502502
instruction_id cannot be reused for new queue.
503503
"""
504504
with self._receive_lock:
505-
self._received.pop(instruction_id)
505+
# Per-instruction read queue may or may not be created yet when
506+
# we mark an instruction as 'cleaned up' when creating
507+
# a bundle processor failed, e.g. due to a flake in DoFn.setup().
508+
# We want to mark an instruction as cleaned up regardless.
509+
self._received.pop(instruction_id, None)
506510
self._cleaned_instruction_ids[instruction_id] = True
507511
while len(self._cleaned_instruction_ids) > _MAX_CLEANED_INSTRUCTIONS:
508512
self._cleaned_instruction_ids.popitem(last=False)
@@ -787,6 +791,12 @@ def close(self):
787791
"""Close all channels that this factory owns."""
788792
raise NotImplementedError(type(self))
789793

794+
def cleanup(self, instruction_id):
795+
# type: (str) -> None
796+
797+
"""Clean up resources for a given instruction."""
798+
pass
799+
790800

791801
class GrpcClientDataChannelFactory(DataChannelFactory):
792802
"""A factory for ``GrpcClientDataChannel``.
@@ -851,10 +861,15 @@ def create_data_channel(self, remote_grpc_port):
851861
def close(self):
852862
# type: () -> None
853863
_LOGGER.info('Closing all cached grpc data channels.')
854-
for _, channel in self._data_channel_cache.items():
864+
for channel in list(self._data_channel_cache.values()):
855865
channel.close()
856866
self._data_channel_cache.clear()
857867

868+
def cleanup(self, instruction_id):
869+
# type: (str) -> None
870+
for channel in list(self._data_channel_cache.values()):
871+
channel._clean_receiving_queue(instruction_id)
872+
858873

859874
class InMemoryDataChannelFactory(DataChannelFactory):
860875
"""A singleton factory for ``InMemoryDataChannel``."""

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -563,15 +563,18 @@ def discard(self, instruction_id, exception):
563563
"""
564564
Marks the instruction id as failed shutting down the ``BundleProcessor``.
565565
"""
566+
processor = None
566567
with self._lock:
567568
self.failed_instruction_ids[instruction_id] = exception
568569
while len(self.failed_instruction_ids) > MAX_FAILED_INSTRUCTIONS:
569570
self.failed_instruction_ids.popitem(last=False)
570-
processor = self.active_bundle_processors[instruction_id][1]
571-
del self.active_bundle_processors[instruction_id]
571+
if instruction_id in self.active_bundle_processors:
572+
processor = self.active_bundle_processors.pop(instruction_id)[1]
572573

573574
# Perform the shutdown while not holding the lock.
574-
processor.shutdown()
575+
if processor:
576+
processor.shutdown()
577+
self.data_channel_factory.cleanup(instruction_id)
575578

576579
def release(self, instruction_id):
577580
# type: (str) -> None
@@ -694,9 +697,9 @@ def process_bundle(
694697
instruction_id # type: str
695698
):
696699
# type: (...) -> beam_fn_api_pb2.InstructionResponse
697-
bundle_processor = self.bundle_processor_cache.get(
698-
instruction_id, request.process_bundle_descriptor_id)
699700
try:
701+
bundle_processor = self.bundle_processor_cache.get(
702+
instruction_id, request.process_bundle_descriptor_id)
700703
with bundle_processor.state_handler.process_instruction_id(
701704
instruction_id, request.cache_tokens):
702705
with self.maybe_profile(instruction_id):

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

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from apache_beam.portability.api import beam_fn_api_pb2_grpc
3838
from apache_beam.portability.api import beam_runner_api_pb2
3939
from apache_beam.portability.api import metrics_pb2
40+
from apache_beam.runners.worker import data_plane
4041
from apache_beam.runners.worker import sdk_worker
4142
from apache_beam.runners.worker import statecache
4243
from apache_beam.runners.worker.sdk_worker import BundleProcessorCache
@@ -126,7 +127,10 @@ def test_fn_registration(self):
126127

127128
def test_inactive_bundle_processor_returns_empty_progress_response(self):
128129
bundle_processor = mock.MagicMock()
129-
bundle_processor_cache = BundleProcessorCache(None, None, None, {})
130+
data_channel_factory = mock.create_autospec(
131+
data_plane.GrpcClientDataChannelFactory)
132+
bundle_processor_cache = BundleProcessorCache(
133+
None, None, data_channel_factory, {})
130134
bundle_processor_cache.activate('instruction_id')
131135
worker = SdkWorker(bundle_processor_cache)
132136
split_request = beam_fn_api_pb2.InstructionRequest(
@@ -153,7 +157,10 @@ def test_inactive_bundle_processor_returns_empty_progress_response(self):
153157

154158
def test_failed_bundle_processor_returns_failed_progress_response(self):
155159
bundle_processor = mock.MagicMock()
156-
bundle_processor_cache = BundleProcessorCache(None, None, None, {})
160+
data_channel_factory = mock.create_autospec(
161+
data_plane.GrpcClientDataChannelFactory)
162+
bundle_processor_cache = BundleProcessorCache(
163+
None, None, data_channel_factory, {})
157164
bundle_processor_cache.activate('instruction_id')
158165
worker = SdkWorker(bundle_processor_cache)
159166

@@ -176,7 +183,10 @@ def test_failed_bundle_processor_returns_failed_progress_response(self):
176183

177184
def test_inactive_bundle_processor_returns_empty_split_response(self):
178185
bundle_processor = mock.MagicMock()
179-
bundle_processor_cache = BundleProcessorCache(None, None, None, {})
186+
data_channel_factory = mock.create_autospec(
187+
data_plane.GrpcClientDataChannelFactory)
188+
bundle_processor_cache = BundleProcessorCache(
189+
None, None, data_channel_factory, {})
180190
bundle_processor_cache.activate('instruction_id')
181191
worker = SdkWorker(bundle_processor_cache)
182192
split_request = beam_fn_api_pb2.InstructionRequest(
@@ -262,7 +272,10 @@ def test_harness_monitoring_infos_and_metadata(self):
262272

263273
def test_failed_bundle_processor_returns_failed_split_response(self):
264274
bundle_processor = mock.MagicMock()
265-
bundle_processor_cache = BundleProcessorCache(None, None, None, {})
275+
data_channel_factory = mock.create_autospec(
276+
data_plane.GrpcClientDataChannelFactory)
277+
bundle_processor_cache = BundleProcessorCache(
278+
None, None, data_channel_factory, {})
266279
bundle_processor_cache.activate('instruction_id')
267280
worker = SdkWorker(bundle_processor_cache)
268281

@@ -338,6 +351,29 @@ def stop(self):
338351

339352
self.assertEqual(response, expected_response)
340353

354+
def test_bundle_processor_creation_failure_cleans_up_grpc_data_channel(self):
355+
data_channel_factory = data_plane.GrpcClientDataChannelFactory()
356+
channel = data_channel_factory.create_data_channel_from_url('some_url')
357+
state_handler_factory = mock.create_autospec(
358+
sdk_worker.GrpcStateHandlerFactory)
359+
bundle_processor_cache = BundleProcessorCache(
360+
frozenset(), state_handler_factory, data_channel_factory, {})
361+
if bundle_processor_cache.periodic_shutdown:
362+
bundle_processor_cache.periodic_shutdown.cancel()
363+
364+
bundle_processor_cache.get = mock.MagicMock(
365+
side_effect=RuntimeError('test error'))
366+
367+
worker = SdkWorker(bundle_processor_cache)
368+
instruction_id = 'instruction_id'
369+
request = beam_fn_api_pb2.ProcessBundleRequest(
370+
process_bundle_descriptor_id='descriptor_id')
371+
372+
with self.assertRaises(RuntimeError):
373+
worker.process_bundle(request, instruction_id)
374+
375+
self.assertIn(instruction_id, channel._cleaned_instruction_ids)
376+
341377

342378
class CachingStateHandlerTest(unittest.TestCase):
343379
def test_caching(self):

0 commit comments

Comments
 (0)