-
Notifications
You must be signed in to change notification settings - Fork 18.2k
refactor(coordination): reliable Redis Streams await/notify (replace at-most-once pub/sub) #43409
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cde9cec
7bb5108
6e2274f
c2b6546
6051274
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -154,6 +154,47 @@ def xrange( | |
| count = count or self.MAX_EVENT_COUNT | ||
| return self._cache.xrange(stream_name, start, end, count) | ||
|
|
||
| def xread( | ||
| self, | ||
| streams: dict[str, str], | ||
| count: int | None = None, | ||
| block_ms: int | None = None, | ||
| ) -> list[Any]: | ||
| """ | ||
| Read new entries from one or more streams, optionally blocking. | ||
|
|
||
| Reliable, event-driven delivery: pass the last id already seen per stream | ||
| and this returns only entries added *after* it — so a signal is never | ||
| missed, even across reconnects (unlike pub/sub). ``block_ms`` blocks up to | ||
| that many milliseconds waiting for a new entry (``None``/``0`` returns | ||
| immediately). | ||
|
|
||
| :param streams: mapping of ``{stream_name: last_id_seen}`` | ||
| :param count: max entries to return | ||
| :param block_ms: milliseconds to block for a new entry (``None`` = no block) | ||
| :returns: redis-py XREAD reply — ``[[stream, [(id, {field: value}), ...]]]`` | ||
| — or an empty list when nothing arrived before the block elapsed | ||
| """ | ||
| return self._cache.xread(streams, count=count, block=block_ms) or [] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: The helper forwards a blocking Severity Level: Major
|
||
|
|
||
| def stream_last_id(self, stream_name: str) -> str: | ||
| """ | ||
| Return the id of the last entry in a stream, or ``"0-0"`` if it is empty. | ||
|
|
||
| Used to capture a baseline before waiting so a subsequent blocking | ||
| :meth:`xread` from that id catches every entry added afterwards — closing | ||
| the publish-before-subscribe race that pub/sub cannot. | ||
| """ | ||
| entries = self._cache.xrevrange(stream_name, count=1) | ||
| if not entries: | ||
| return "0-0" | ||
| last_id = entries[0][0] | ||
| return last_id.decode() if isinstance(last_id, bytes) else last_id | ||
|
|
||
| def expire(self, name: str, seconds: int) -> bool: | ||
| """Set a TTL (seconds) on a key; used to bound signal-stream growth.""" | ||
| return bool(self._cache.expire(name, seconds)) | ||
|
|
||
| @classmethod | ||
| def from_config(cls, config: dict[str, Any]) -> RedisCacheBackend: | ||
| kwargs = { | ||
|
|
@@ -347,6 +388,29 @@ def xrange( | |
| count = count or self.MAX_EVENT_COUNT | ||
| return self._cache.xrange(stream_name, start, end, count) | ||
|
|
||
| def xread( | ||
| self, | ||
| streams: dict[str, str], | ||
| count: int | None = None, | ||
| block_ms: int | None = None, | ||
| ) -> list[Any]: | ||
| """Reliable, optionally-blocking stream read (see | ||
| :meth:`RedisCacheBackend.xread`).""" | ||
| return self._cache.xread(streams, count=count, block=block_ms) or [] | ||
|
|
||
| def stream_last_id(self, stream_name: str) -> str: | ||
| """Return the last entry id, or ``"0-0"`` if empty (see | ||
| :meth:`RedisCacheBackend.stream_last_id`).""" | ||
| entries = self._cache.xrevrange(stream_name, count=1) | ||
| if not entries: | ||
| return "0-0" | ||
| last_id = entries[0][0] | ||
| return last_id.decode() if isinstance(last_id, bytes) else last_id | ||
|
|
||
| def expire(self, name: str, seconds: int) -> bool: | ||
| """Set a TTL (seconds) on a key; used to bound signal-stream growth.""" | ||
| return bool(self._cache.expire(name, seconds)) | ||
|
|
||
| @classmethod | ||
| def from_config(cls, config: dict[str, Any]) -> RedisSentinelCacheBackend: | ||
| kwargs = { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: The retention description incorrectly states that each stream keeps only its latest entry. The coordination backend passes
MAXLENwithout disabling Redis-py's default approximate trimming, so Redis may retain more than one entry. Document this as an approximate bound or change the implementation to request exact trimming if retaining exactly one entry is required. [docstring mismatch]Severity Level: Minor 🧹
Prompt for AI Agent 🤖