Poll deadlines with ZRANGE BYSCORE instead of the deprecated ZRANGEBYSCORE
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user