Skip to content

Commit 5053397

Browse files
committed
fix: Await in-flight poll before closing the transport on stop
AsyncRepeatingTask gains wait_stopped() to await the cancelled task; the polling processor's stop() now waits for the in-flight poll to unwind before closing the requester's transport, so awaiting stop() guarantees background work has stopped and the transport isn't closed under a live request.
1 parent 216f8aa commit 5053397

3 files changed

Lines changed: 44 additions & 0 deletions

File tree

ldclient/impl/aio/concurrency.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,16 @@ def stop(self):
223223
if task is not None and task is not asyncio.current_task():
224224
task.cancel()
225225

226+
async def wait_stopped(self):
227+
"""Waits for the task to finish unwinding after ``stop()``. A no-op if
228+
the task never started or is the current task."""
229+
task = self.__task
230+
if task is not None and task is not asyncio.current_task():
231+
try:
232+
await task
233+
except asyncio.CancelledError:
234+
pass
235+
226236
async def _run(self):
227237
try:
228238
if self.__initial_delay > 0:

ldclient/impl/datasource/async_polling.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ def initialized(self):
4646

4747
async def stop(self):
4848
self.__stop_with_error_info(None)
49+
# Wait for the in-flight poll to finish unwinding before closing the
50+
# transport, so awaiting stop() guarantees background work has stopped
51+
# and we don't close the HTTP transport out from under a live request.
52+
await self._task.wait_stopped()
4953
await self._requester.close()
5054

5155
def __stop_with_error_info(self, error: Optional[DataSourceErrorInfo]):

ldclient/testing/impl/datasource/test_async_polling.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,36 @@ async def test_stop_closes_requester(self):
298298

299299
processor._requester.close.assert_awaited_once()
300300

301+
@pytest.mark.asyncio
302+
async def test_stop_awaits_poll_before_closing_transport(self):
303+
# The in-flight poll must finish unwinding before the transport is
304+
# closed, so we never close it out from under a live request.
305+
order = []
306+
started = asyncio.Event()
307+
308+
async def slow_poll():
309+
started.set()
310+
try:
311+
await asyncio.sleep(60)
312+
except asyncio.CancelledError:
313+
order.append('poll_done')
314+
raise
315+
316+
async def close():
317+
order.append('transport_closed')
318+
319+
requester = MagicMock()
320+
requester.get_all_data = slow_poll
321+
requester.close = close
322+
323+
processor = make_processor(requester=requester)
324+
processor.start()
325+
await asyncio.wait_for(started.wait(), timeout=1.0)
326+
327+
await processor.stop()
328+
329+
assert order == ['poll_done', 'transport_closed']
330+
301331
@pytest.mark.asyncio
302332
async def test_stop_cancels_polling_task_cleanly(self):
303333
store = MockAsyncFeatureStore()

0 commit comments

Comments
 (0)