From 21911caf9e2734666ac8938f7947318ac3c7ed61 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Sun, 27 Sep 2026 21:46:02 +0000 Subject: [PATCH] Poll deadlines with ZRANGE BYSCORE instead of the deprecated ZRANGEBYSCORE --- .../src/tavolo/platform/store.py | 11 ++++- .../tavolo-platform/tests/test_store.py | 49 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/server/packages/tavolo-platform/src/tavolo/platform/store.py b/server/packages/tavolo-platform/src/tavolo/platform/store.py index fa242c5..1fa1664 100644 --- a/server/packages/tavolo-platform/src/tavolo/platform/store.py +++ b/server/packages/tavolo-platform/src/tavolo/platform/store.py @@ -244,10 +244,17 @@ class RedisGameStore(GameStore): await self._redis.zadd(DEADLINES_KEY, {member: due_at}) async def due_deadlines(self, now: float, limit: int = 32) -> List[str]: + # ``ZRANGE ... BYSCORE`` is the non-deprecated spelling of the + # old ``ZRANGEBYSCORE``. members = cast( list, - await self._redis.zrangebyscore( - DEADLINES_KEY, "-inf", now, start=0, num=limit + await self._redis.zrange( + DEADLINES_KEY, + "-inf", + now, + byscore=True, + offset=0, + num=limit, ), ) return [m.decode("utf-8") if isinstance(m, bytes) else m for m in members] diff --git a/server/packages/tavolo-platform/tests/test_store.py b/server/packages/tavolo-platform/tests/test_store.py index 5b64236..dadfd2d 100644 --- a/server/packages/tavolo-platform/tests/test_store.py +++ b/server/packages/tavolo-platform/tests/test_store.py @@ -4,10 +4,12 @@ from __future__ import annotations import asyncio import time import unittest +from typing import Any, Optional, cast from tavolo.platform import GameSession, Seat from tavolo.platform.registry import GameRegistry from tavolo.platform.store import ( + DEADLINES_KEY, ONGOING_GAMES_KEY, InMemoryGameStore, RedisGameStore, @@ -75,10 +77,39 @@ class _FakeRedis: def __init__(self) -> None: self.keys: dict = {} self.zsets: dict = {} + self.zrange_calls: list = [] def pipeline(self, transaction: bool = True) -> _FakePipeline: return _FakePipeline(self) + async def zadd(self, key: str, mapping: dict) -> int: + return cast(int, self.apply(("zadd", key, mapping))) + + async def zrem(self, key: str, member: str) -> int: + return cast(int, self.apply(("zrem", key, member))) + + async def zrange( + self, + name: str, + start: Any, + end: Any, + byscore: bool = False, + offset: Optional[int] = None, + num: Optional[int] = None, + **kwargs: object, + ) -> list: + self.zrange_calls.append((name, start, end, byscore, offset, num)) + members = sorted( + self.zsets.get(name, {}).items(), key=lambda item: item[1] + ) + if byscore: + low = float("-inf") if start == "-inf" else float(start) + high = float("inf") if end == "+inf" else float(end) + members = [item for item in members if low <= item[1] <= high] + if offset is not None and num is not None: + members = members[offset : offset + num] + return [member for member, _ in members] + def apply(self, op: tuple) -> object: name, *args = op if name == "set": @@ -269,6 +300,24 @@ class RedisGameStoreTest(unittest.TestCase): self.assertEqual(1, await store.count_ongoing()) self.assertNotIn("stale", redis.zsets[ONGOING_GAMES_KEY]) + @async_test + async def test_due_deadlines_uses_zrange_byscore(self) -> None: + redis = _FakeRedis() + store = RedisGameStore(redis, _registry()) # type: ignore[arg-type] + await store.add_deadline("b", due_at=50.0) + await store.add_deadline("a", due_at=10.0) + await store.add_deadline("c", due_at=200.0) + # Re-adding an existing member only updates its due time. + await store.add_deadline("b", due_at=60.0) + + self.assertEqual(["a"], await store.due_deadlines(now=20.0)) + self.assertEqual( + [(DEADLINES_KEY, "-inf", 20.0, True, 0, 32)], redis.zrange_calls + ) + # Matches come back in due-time order and honour the limit. + self.assertEqual(["a"], await store.due_deadlines(now=100.0, limit=1)) + self.assertEqual(["a", "b"], await store.due_deadlines(now=100.0)) + if __name__ == "__main__": unittest.main()