Skip to content

Commit ad0424c

Browse files
committed
Adjust async streaming response stop() method to have a timeout argument, and use it when exiting context manager.
Things sometimes hang in CI and this might help with that. Also adjusted when _ack_queue.task_done() is called (not that anybody seems to be joining on the the queue?).
1 parent e30b598 commit ad0424c

5 files changed

Lines changed: 51 additions & 39 deletions

File tree

kurrentdbclient/common.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def __init__(self, grpc_streamers: AsyncGrpcStreamers) -> None:
167167
self._is_stopped_lock = asyncio.Lock()
168168

169169
@abstractmethod
170-
async def stop(self) -> None:
170+
async def stop(self, *, timeout: float | None = None) -> None:
171171
"""
172172
Stops the iterator(s) of streaming call.
173173
"""
@@ -188,7 +188,7 @@ class AsyncGrpcStreamers(BaseGrpcStreamers[AsyncGrpcStreamer]):
188188
async def close(self) -> None:
189189
for async_grpc_streamer in self:
190190
# print("closing streamer")
191-
await async_grpc_streamer.stop()
191+
await async_grpc_streamer.stop(timeout=1)
192192
# print("closed streamer")
193193

194194

@@ -577,7 +577,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
577577
self._is_context_manager_active = False
578578

579579
@abstractmethod
580-
async def stop(self) -> None:
580+
async def stop(self, *, timeout: float | None = None) -> None:
581581
pass # pragma: no cover
582582

583583
def __aiter__(self) -> Self:
@@ -589,7 +589,7 @@ async def __aenter__(self) -> Self:
589589

590590
async def __aexit__(self, *args: object, **kwargs: Any) -> None:
591591
self._is_context_manager_active = False
592-
await self.stop()
592+
await self.stop(timeout=5)
593593

594594
def _set_iter_error_for_testing(self) -> None:
595595
# This, because I can't find a good way to inspire an error during iterating

kurrentdbclient/instrumentation/opentelemetry/spanners.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -956,8 +956,8 @@ async def __anext__(self) -> RecordedEvent:
956956
span.end()
957957
self._current_span = None
958958

959-
async def stop(self) -> None:
960-
await self.response.stop()
959+
async def stop(self, *, timeout: float | None = None) -> None:
960+
await self.response.stop(timeout=timeout)
961961

962962
async def __aenter__(self) -> Self:
963963
await self.response.__aenter__()

kurrentdbclient/persistent.py

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def __init__(
110110
self._max_ack_delay = max_ack_delay
111111
self._stopping_grace = stopping_grace
112112
self._has_requested_options = False
113+
self._num_constructed_acks = 0
113114
self.subscription_id = b""
114115

115116
def _construct_initial_read_req(self) -> persistent_pb2.ReadReq:
@@ -130,10 +131,10 @@ def _construct_initial_read_req(self) -> persistent_pb2.ReadReq:
130131
options.all.CopyFrom(shared_pb2.Empty())
131132
return persistent_pb2.ReadReq(options=options)
132133

133-
@staticmethod
134134
def _construct_ack_or_nack_read_req(
135-
subscription_id: bytes, event_ids: list[UUID], action: str
135+
self, subscription_id: bytes, event_ids: list[UUID], action: str
136136
) -> persistent_pb2.ReadReq:
137+
self._num_constructed_acks = len(event_ids)
137138
ids = [shared_pb2.UUID(string=str(event_id)) for event_id in event_ids]
138139
if action == "ack":
139140
read_req = persistent_pb2.ReadReq(
@@ -210,17 +211,19 @@ async def __anext__(self) -> persistent_pb2.ReadReq:
210211
# First return read request with options, then return read request
211212
# with batch of n/acks whenever the batch is full, or when the n/ack
212213
# actions changes, or periodically, or when stopping.
213-
214214
if not self._has_requested_options:
215215
# Return initial read request with options.
216216
self._has_requested_options = True
217217
return self._construct_initial_read_req()
218218

219+
# Account on queue for previously returned n/acks.
220+
for _ in range(self._num_constructed_acks):
221+
self._ack_queue.task_done()
222+
self._num_constructed_acks = 0
223+
219224
# Return read request with a batch of n/acks...
220225

221226
# Initialise batch, maybe from held n/ack.
222-
for _ in self._batch_ids:
223-
self._ack_queue.task_done()
224227
self._batch_ids = []
225228
batch_action: str | None = None
226229
if self._ack_held is not None:
@@ -234,7 +237,6 @@ async def __anext__(self) -> persistent_pb2.ReadReq:
234237
if self._is_stopping:
235238
# Allow time for server to process last n/acks.
236239
await asyncio.sleep(self._stopping_grace)
237-
self._ack_queue.task_done()
238240
raise StopAsyncIteration from None
239241

240242
try:
@@ -316,12 +318,11 @@ async def nack(
316318
assert action in ["unknown", "park", "retry", "skip", "stop"]
317319
await self._ack_queue.put((event_id, action))
318320

319-
async def stop(self, *, wait_until_stopped: bool = True) -> None:
321+
async def stop(self, *, timeout: float | None = None) -> None:
320322
if not self._is_poisoned:
321323
self._is_poisoned = True
322324
await self._ack_queue.put((None, "poison"))
323-
if wait_until_stopped:
324-
await self._is_stopped.wait()
325+
await asyncio.wait_for(self._is_stopped.wait(), timeout)
325326

326327

327328
class SubscriptionReadReqs(BaseSubscriptionReadReqs):
@@ -364,6 +365,11 @@ def __next__(self) -> persistent_pb2.ReadReq:
364365
self._has_requested_options = True
365366
return self._construct_initial_read_req()
366367

368+
# Account on queue for previously returned n/acks.
369+
for _ in range(self._num_constructed_acks):
370+
self._ack_queue.task_done()
371+
self._num_constructed_acks = 0
372+
367373
# Send a batch of n/acks...
368374

369375
# Initialise batch, maybe from held n/ack.
@@ -387,7 +393,6 @@ def __next__(self) -> persistent_pb2.ReadReq:
387393
# Wait for next n/ack, timeout with "max ack delay".
388394
get_timeout = max(0.0, self._calc_time_until_next_ack_batch())
389395
event_id, action = self._ack_queue.get(timeout=get_timeout)
390-
self._ack_queue.task_done()
391396

392397
# If queue was poisoned, send non-empty batch now.
393398
if action == "poison":
@@ -572,7 +577,7 @@ async def init(self) -> None:
572577
msg = f"Expected subscription confirmation, got: {first_read_resp}"
573578
raise SubscriptionConfirmationError(msg)
574579
except BaseException:
575-
await self.stop(wait_until_stopped=False)
580+
await self.stop(timeout=1)
576581
raise
577582

578583
@property
@@ -600,26 +605,27 @@ async def __anext__(self) -> RecordedEvent:
600605
else: # pragma: no cover
601606
pass
602607
except BaseException:
603-
await self.stop(wait_until_stopped=False)
608+
await self.stop(timeout=1)
604609
raise
605610

606-
async def stop(self, *, wait_until_stopped: bool = True) -> None:
611+
async def stop(self, *, timeout: float | None = None) -> None:
607612
if self._is_context_manager_active:
608613
self._is_stopping = True
609614
elif not await self._set_is_stopped():
610-
await self._read_reqs.stop(wait_until_stopped=wait_until_stopped)
611-
self._read_resp_stream_worker_task.cancel()
612-
self._stream_stream_call.cancel()
613-
await asyncio.sleep(0.05)
614-
self._grpc_streamers.remove(self)
615615
try:
616-
await self._read_resp_stream_worker_task
617-
except asyncio.CancelledError:
618-
pass
619-
except grpc.RpcError as e:
620-
raise handle_rpc_error(e) from e
621-
if self._read_reqs.errored:
622-
raise self._read_reqs.errored
616+
await self._read_reqs.stop(timeout=timeout)
617+
finally:
618+
self._read_resp_stream_worker_task.cancel()
619+
self._stream_stream_call.cancel()
620+
self._grpc_streamers.remove(self)
621+
try:
622+
await self._read_resp_stream_worker_task
623+
except asyncio.CancelledError:
624+
pass
625+
except grpc.RpcError as e:
626+
raise handle_rpc_error(e) from e
627+
if self._read_reqs.errored:
628+
raise self._read_reqs.errored
623629

624630
async def ack(self, item: UUID | RecordedEvent) -> None:
625631
await self._read_reqs.ack(event_id=self._get_event_id(item))

kurrentdbclient/streams.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ async def __anext__(self) -> RecordedEvent:
225225
await self.stop()
226226
raise
227227

228-
async def stop(self, *, wait_until_stopped: bool = True) -> None:
228+
async def stop(self, *, timeout: float | None = None) -> None:
229229
if self._is_context_manager_active:
230230
self._is_stopping = True
231231
elif not await self._set_is_stopped():

tests/test_client_async.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2752,12 +2752,8 @@ async def test_persistent_subscription_read_reqs(self) -> None:
27522752
req5 = await reqs.__anext__()
27532753
self.assertEqual(len(req5.ack.ids), 2)
27542754

2755-
# Cover the case of stopping without waiting (wait_until_stopped=False).
2756-
reqs = AsyncSubscriptionReadReqs("group1", max_ack_batch_size=3)
2757-
await reqs.stop(wait_until_stopped=False)
2758-
2759-
# Cover the case of calling this method twice.
2760-
await reqs.stop(wait_until_stopped=False)
2755+
# Cover the case of calling stop() twice.
2756+
await reqs.stop()
27612757

27622758
# Iterate until stopped.
27632759
async for _ in reqs:
@@ -2772,10 +2768,20 @@ async def iterate_until_stopped() -> None:
27722768

27732769
async def sleep_then_stop() -> None:
27742770
await asyncio.sleep(1)
2775-
await reqs.stop(wait_until_stopped=False)
2771+
await reqs.stop()
27762772

27772773
await asyncio.gather(iterate_until_stopped(), sleep_then_stop())
27782774

2775+
# Iterate and stop with timeout when request iterator is not being called.
2776+
reqs = AsyncSubscriptionReadReqs("group1", max_ack_batch_size=3)
2777+
await reqs.ack(uuid4())
2778+
2779+
async def sleep_then_stop_with_timeout() -> None:
2780+
await reqs.stop(timeout=1)
2781+
2782+
with self.assertRaises(TimeoutError):
2783+
await sleep_then_stop_with_timeout()
2784+
27792785
# Can't call ack() after stopped.
27802786
with self.assertRaises(ProgrammingError):
27812787
await reqs.ack(uuid4())

0 commit comments

Comments
 (0)