From a19307dc61e36258e2a3a2aba5de7a1b160a7666 Mon Sep 17 00:00:00 2001 From: Fielding Johnston Date: Sun, 22 Dec 2024 16:34:03 -0600 Subject: [PATCH 1/2] allow for watching future values with watcher Include StopIterSentinel oops --- nats/js/kv.py | 22 ++++++++++++++++------ nats/js/object_store.py | 14 ++++++++------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/nats/js/kv.py b/nats/js/kv.py index 6f8cd28c..46230c5a 100644 --- a/nats/js/kv.py +++ b/nats/js/kv.py @@ -35,6 +35,12 @@ logger = logging.getLogger(__name__) +class StopIterSentinel: + """A sentinel class used to indicate that iteration should stop.""" + + pass + + class KeyValue: """ KeyValue uses the JetStream KeyValue functionality. @@ -275,6 +281,9 @@ async def purge_deletes(self, olderthan: int = 30 * 60) -> bool: watcher = await self.watchall() delete_markers = [] async for update in watcher: + if update is None: + break + if update.operation == KV_DEL or update.operation == KV_PURGE: delete_markers.append(update) @@ -299,11 +308,11 @@ async def status(self) -> BucketStatus: return KeyValue.BucketStatus(stream_info=info, bucket=self._name) class KeyWatcher: + STOP_ITER = StopIterSentinel() def __init__(self, js): self._js = js - self._updates: asyncio.Queue[KeyValue.Entry - | None] = asyncio.Queue(maxsize=256) + self._updates: asyncio.Queue[KeyValue.Entry | None | StopIterSentinel] = asyncio.Queue(maxsize=256) self._sub = None self._pending: Optional[int] = None @@ -316,6 +325,7 @@ async def stop(self): stop will stop this watcher. """ await self._sub.unsubscribe() + await self._updates.put(KeyValue.KeyWatcher.STOP_ITER) async def updates(self, timeout=5.0): """ @@ -330,10 +340,10 @@ def __aiter__(self): return self async def __anext__(self): - entry = await self._updates.get() - if not entry: - raise StopAsyncIteration - else: + while True: + entry = await self._updates.get() + if isinstance(entry, StopIterSentinel): + raise StopAsyncIteration return entry async def watchall(self, **kwargs) -> KeyWatcher: diff --git a/nats/js/object_store.py b/nats/js/object_store.py index 70ce3d3b..c8edcc8a 100644 --- a/nats/js/object_store.py +++ b/nats/js/object_store.py @@ -33,7 +33,7 @@ ObjectDeletedError, ObjectNotFoundError, ) -from nats.js.kv import MSG_ROLLUP_SUBJECT +from nats.js.kv import MSG_ROLLUP_SUBJECT, StopIterSentinel VALID_BUCKET_RE = re.compile(r"^[a-zA-Z0-9_-]+$") VALID_KEY_RE = re.compile(r"^[-/_=\.a-zA-Z0-9]+$") @@ -424,10 +424,11 @@ async def update_meta( await self._js.purge_stream(self._stream, subject=meta_subj) class ObjectWatcher: + STOP_ITER = StopIterSentinel() def __init__(self, js): self._js = js - self._updates = asyncio.Queue(maxsize=256) + self._updates: asyncio.Queue[Union[api.ObjectInfo, None, StopIterSentinel]] = asyncio.Queue(maxsize=256) self._sub = None self._pending: Optional[int] = None @@ -454,10 +455,11 @@ def __aiter__(self): return self async def __anext__(self): - entry = await self._updates.get() - if not entry: - raise StopAsyncIteration - else: + while True: + entry = await self._updates.get() + + if isinstance(entry, StopIterSentinel): + raise StopAsyncIteration return entry async def watch( From 8e931c3a2e869e1401d12d8d55896b77fd25ba0a Mon Sep 17 00:00:00 2001 From: Fielding Johnston Date: Thu, 13 Mar 2025 13:25:39 -0500 Subject: [PATCH 2/2] fix formatting --- nats/js/kv.py | 5 ++++- nats/js/object_store.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/nats/js/kv.py b/nats/js/kv.py index 46230c5a..f7571a26 100644 --- a/nats/js/kv.py +++ b/nats/js/kv.py @@ -312,7 +312,10 @@ class KeyWatcher: def __init__(self, js): self._js = js - self._updates: asyncio.Queue[KeyValue.Entry | None | StopIterSentinel] = asyncio.Queue(maxsize=256) + self._updates: asyncio.Queue[KeyValue.Entry | None + | StopIterSentinel] = asyncio.Queue( + maxsize=256 + ) self._sub = None self._pending: Optional[int] = None diff --git a/nats/js/object_store.py b/nats/js/object_store.py index c8edcc8a..ad5added 100644 --- a/nats/js/object_store.py +++ b/nats/js/object_store.py @@ -428,7 +428,9 @@ class ObjectWatcher: def __init__(self, js): self._js = js - self._updates: asyncio.Queue[Union[api.ObjectInfo, None, StopIterSentinel]] = asyncio.Queue(maxsize=256) + self._updates: asyncio.Queue[Union[api.ObjectInfo, None, + StopIterSentinel] + ] = asyncio.Queue(maxsize=256) self._sub = None self._pending: Optional[int] = None