Poll deadlines with ZRANGE BYSCORE instead of the deprecated ZRANGEBYSCORE

This commit is contained in:
2026-09-28 05:50:54 +08:00
parent 08f158ef28
commit 21911caf9e
2 changed files with 58 additions and 2 deletions
@@ -244,10 +244,17 @@ class RedisGameStore(GameStore):
await self._redis.zadd(DEADLINES_KEY, {member: due_at}) await self._redis.zadd(DEADLINES_KEY, {member: due_at})
async def due_deadlines(self, now: float, limit: int = 32) -> List[str]: 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( members = cast(
list, list,
await self._redis.zrangebyscore( await self._redis.zrange(
DEADLINES_KEY, "-inf", now, start=0, num=limit 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] return [m.decode("utf-8") if isinstance(m, bytes) else m for m in members]
@@ -4,10 +4,12 @@ from __future__ import annotations
import asyncio import asyncio
import time import time
import unittest import unittest
from typing import Any, Optional, cast
from tavolo.platform import GameSession, Seat from tavolo.platform import GameSession, Seat
from tavolo.platform.registry import GameRegistry from tavolo.platform.registry import GameRegistry
from tavolo.platform.store import ( from tavolo.platform.store import (
DEADLINES_KEY,
ONGOING_GAMES_KEY, ONGOING_GAMES_KEY,
InMemoryGameStore, InMemoryGameStore,
RedisGameStore, RedisGameStore,
@@ -75,10 +77,39 @@ class _FakeRedis:
def __init__(self) -> None: def __init__(self) -> None:
self.keys: dict = {} self.keys: dict = {}
self.zsets: dict = {} self.zsets: dict = {}
self.zrange_calls: list = []
def pipeline(self, transaction: bool = True) -> _FakePipeline: def pipeline(self, transaction: bool = True) -> _FakePipeline:
return _FakePipeline(self) 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: def apply(self, op: tuple) -> object:
name, *args = op name, *args = op
if name == "set": if name == "set":
@@ -269,6 +300,24 @@ class RedisGameStoreTest(unittest.TestCase):
self.assertEqual(1, await store.count_ongoing()) self.assertEqual(1, await store.count_ongoing())
self.assertNotIn("stale", redis.zsets[ONGOING_GAMES_KEY]) 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__": if __name__ == "__main__":
unittest.main() unittest.main()