Skip to content

Commit c1272ba

Browse files
authored
feat: status on cache are now actually ephemeral (#105)
1 parent 369d278 commit c1272ba

File tree

5 files changed

+68
-7
lines changed

5 files changed

+68
-7
lines changed

src/baby_serverlist/models.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
from django.db import models
77

88
from accounts.models import Account
9-
from commons.cache import get_baby_server_heartbeat
9+
from commons.cache import BABY_SERVER_HEARTBEAT_TTL_SECONDS, get_baby_server_heartbeat
1010

1111
SERVERLIST_TOKEN_SALT = "baby_serverlist.serverlist_token"
12+
LIVE_HEARTBEAT_GRACE_SECONDS = 2
1213

1314

1415
class BabyServer(models.Model):
@@ -40,7 +41,7 @@ def generate_serverlist_token(self) -> str:
4041
return signing.dumps(payload, salt=SERVERLIST_TOKEN_SALT)
4142

4243
def is_live(self) -> bool:
43-
"""Return True when the server has reported within the last 12 seconds."""
44+
"""Return True when the server has reported within the heartbeat TTL window."""
4445
heartbeat_iso = get_baby_server_heartbeat(str(self.id))
4546
if not heartbeat_iso:
4647
return False
@@ -50,4 +51,7 @@ def is_live(self) -> bool:
5051
return False
5152
if heartbeat_time.tzinfo is None:
5253
heartbeat_time = heartbeat_time.replace(tzinfo=UTC)
53-
return datetime.now(tz=UTC) - heartbeat_time <= timedelta(seconds=12)
54+
55+
# live if last heartbeat within the cache TTL plus a small grace buffer
56+
ttl_with_grace = BABY_SERVER_HEARTBEAT_TTL_SECONDS + LIVE_HEARTBEAT_GRACE_SECONDS
57+
return datetime.now(tz=UTC) - heartbeat_time <= timedelta(seconds=ttl_with_grace)

src/commons/cache.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
from django.core.cache import cache
55

6+
BABY_SERVER_STATUS_TTL_SECONDS = 10
7+
BABY_SERVER_HEARTBEAT_TTL_SECONDS = 10
8+
69
SERVER_STATUS_KEY_PREFIX = "baby_server_status:"
710
SERVER_HEARTBEAT_KEY_PREFIX = "baby_server_heartbeat:"
811

@@ -19,7 +22,7 @@ def _heartbeat_key(server_id: str) -> str:
1922

2023
def set_baby_server_status(server_id: str, status: dict[str, Any]) -> None:
2124
"""Persist the latest status payload for a server."""
22-
cache.set(_status_key(server_id), status)
25+
cache.set(_status_key(server_id), status, timeout=BABY_SERVER_STATUS_TTL_SECONDS)
2326

2427

2528
def get_baby_server_status(server_id: str) -> dict[str, Any] | None:
@@ -39,7 +42,7 @@ def get_many_baby_server_statuses(server_ids: Iterable[str]) -> dict[str, dict[s
3942

4043
def set_baby_server_heartbeat(server_id: str, timestamp: str) -> None:
4144
"""Persist the last-reported timestamp for a server."""
42-
cache.set(_heartbeat_key(server_id), timestamp)
45+
cache.set(_heartbeat_key(server_id), timestamp, timeout=BABY_SERVER_HEARTBEAT_TTL_SECONDS)
4346

4447

4548
def get_baby_server_heartbeat(server_id: str) -> str | None:

src/tests/baby_serverlist/test_api.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
from rest_framework.test import APITestCase
88

99
from accounts.models import Account
10-
from baby_serverlist.models import BabyServer
10+
from baby_serverlist.models import LIVE_HEARTBEAT_GRACE_SECONDS, BabyServer
1111
from commons.cache import (
12+
BABY_SERVER_HEARTBEAT_TTL_SECONDS,
1213
get_baby_server_heartbeat,
1314
get_baby_server_status,
1415
set_baby_server_heartbeat,
@@ -154,7 +155,9 @@ def test_list_owned_baby_servers_live_flag(self) -> None:
154155
response = self.client.get(reverse("baby_serverlist:list-owned"))
155156
self.assertTrue(response.json()[0]["live"])
156157

157-
stale_time = datetime.now(tz=UTC) - timedelta(seconds=13)
158+
stale_time = datetime.now(tz=UTC) - timedelta(
159+
seconds=BABY_SERVER_HEARTBEAT_TTL_SECONDS + LIVE_HEARTBEAT_GRACE_SECONDS + 1
160+
)
158161
set_baby_server_heartbeat(str(baby_server.id), stale_time.isoformat())
159162

160163
response = self.client.get(reverse("baby_serverlist:list-owned"))
@@ -184,3 +187,20 @@ def test_list_baby_servers_ignores_non_whitelisted(self) -> None:
184187

185188
self.assertEqual(response.status_code, status.HTTP_200_OK)
186189
self.assertEqual(response.json(), {"servers": []})
190+
191+
def test_baby_server_is_live_respects_heartbeat_ttl(self) -> None:
192+
baby_server = BabyServer.objects.create(owner=self.user)
193+
194+
fresh_time = datetime.now(tz=UTC) - timedelta(
195+
seconds=BABY_SERVER_HEARTBEAT_TTL_SECONDS + LIVE_HEARTBEAT_GRACE_SECONDS - 1
196+
)
197+
set_baby_server_heartbeat(str(baby_server.id), fresh_time.isoformat())
198+
self.assertTrue(baby_server.is_live())
199+
200+
stale_time = datetime.now(tz=UTC) - timedelta(
201+
seconds=BABY_SERVER_HEARTBEAT_TTL_SECONDS + LIVE_HEARTBEAT_GRACE_SECONDS + 1
202+
)
203+
set_baby_server_heartbeat(str(baby_server.id), stale_time.isoformat())
204+
stored = get_baby_server_heartbeat(str(baby_server.id))
205+
self.assertEqual(stored, stale_time.isoformat())
206+
self.assertFalse(baby_server.is_live())

src/tests/commons/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

src/tests/commons/test_cache.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from unittest.mock import patch
2+
3+
from django.test import SimpleTestCase
4+
5+
from commons import cache as cache_module
6+
7+
8+
class CommonsCacheTests(SimpleTestCase):
9+
def test_set_baby_server_status_uses_ephemeral_timeout(self) -> None:
10+
payload = {"ServerName": "test"}
11+
server_id = "server-123"
12+
13+
with patch.object(cache_module, "cache") as fake_cache:
14+
cache_module.set_baby_server_status(server_id, payload)
15+
16+
fake_cache.set.assert_called_once_with(
17+
f"{cache_module.SERVER_STATUS_KEY_PREFIX}{server_id}",
18+
payload,
19+
timeout=cache_module.BABY_SERVER_STATUS_TTL_SECONDS,
20+
)
21+
22+
def test_set_baby_server_heartbeat_uses_ephemeral_timeout(self) -> None:
23+
timestamp = "2024-01-01T00:00:00+00:00"
24+
server_id = "server-456"
25+
26+
with patch.object(cache_module, "cache") as fake_cache:
27+
cache_module.set_baby_server_heartbeat(server_id, timestamp)
28+
29+
fake_cache.set.assert_called_once_with(
30+
f"{cache_module.SERVER_HEARTBEAT_KEY_PREFIX}{server_id}",
31+
timestamp,
32+
timeout=cache_module.BABY_SERVER_HEARTBEAT_TTL_SECONDS,
33+
)

0 commit comments

Comments
 (0)