Skip to content

Commit e7baa8a

Browse files
authored
Merge pull request #24 from PRUNplanner/improve-sse-pubsub-usertracking
improve(sse): remove pubsub leftover, add connection tracking
2 parents c0bcdeb + 52f3ce1 commit e7baa8a

3 files changed

Lines changed: 32 additions & 16 deletions

File tree

backend/analytics/admin.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import time
23

34
import redis
45
from django.apps import apps
@@ -71,6 +72,13 @@ def get_redis_stats():
7172
r = redis.from_url(settings.CACHES['default']['LOCATION'])
7273
info = r.info()
7374

75+
# sse metrics
76+
stats_key = 'stream:active_connections'
77+
# prune stale sesions
78+
r.zremrangebyscore(stats_key, 0, time.time() - 30)
79+
# get active users
80+
sse_users = r.zcard(stats_key)
81+
7482
hits = info.get('keyspace_hits', 0)
7583
misses = info.get('keyspace_misses', 0)
7684
total_reqs = hits + misses
@@ -79,6 +87,7 @@ def get_redis_stats():
7987
return {
8088
'usage': info.get('used_memory_human', '0B'),
8189
'hit_rate': hit_rate,
90+
'active_stream_users': sse_users,
8291
'active_connections': info.get('connected_clients', 0),
8392
'blocked_clients': info.get('blocked_clients', 0),
8493
'fragmentation': info.get('mem_fragmentation_ratio', 0),

backend/gamedata/api/sse.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import asyncio
2+
import time
3+
import uuid
24

35
import structlog
46
from django.conf import settings
@@ -18,19 +20,12 @@
1820
)
1921
@permission_classes([AllowAny])
2022
async def sse_stream_view(request):
21-
raw_channels = request.GET.get('channels', 'beat')
22-
channels = [c.strip() for c in raw_channels.split(',') if c.strip()]
23+
channels = [c.strip() for c in request.GET.get('channels', 'beat').split(',') if c.strip()]
24+
redis_keys = {f'stream:{c}': request.headers.get('Last-Event-ID', '0') for c in channels}
2325

24-
header_id = request.headers.get('Last-Event-ID')
25-
26-
if header_id:
27-
# catch up to browsers last id
28-
last_id = header_id
29-
else:
30-
# get existing history
31-
last_id = '0'
32-
33-
redis_keys = {f'stream:{c}': last_id for c in channels}
26+
# unique connection ID for stats
27+
conn_id = str(uuid.uuid4())
28+
stats_key = 'stream:active_connections'
3429

3530
redis_url = settings.CACHES['default']['LOCATION']
3631

@@ -40,13 +35,14 @@ async def event_generator():
4035

4136
# initiate redis, pubsub and subscribe to channels
4237
redis = await aioredis.from_url(redis_url)
43-
pubsub = redis.pubsub()
44-
await pubsub.subscribe(*channels)
4538

4639
total_sent_count = 0
4740

48-
# loop
4941
try:
42+
# register connection with current timestamp
43+
await redis.zadd(stats_key, {conn_id: time.time()})
44+
45+
# loop
5046
while True:
5147
# XREAD expects a dict with { key: last_id }
5248
events = await redis.xread(redis_keys, count=1, block=5000)
@@ -61,6 +57,8 @@ async def event_generator():
6157
payload = data[b'payload'].decode('utf-8')
6258
yield f'id: {last_id}\ndata: {payload}\n\n'
6359
else:
60+
# keep-alive + heartbeat in redis
61+
await redis.zadd(stats_key, {conn_id: time.time()})
6462
yield ': \n\n'
6563

6664
# user left stream
@@ -69,7 +67,12 @@ async def event_generator():
6967

7068
# cleanup
7169
finally:
72-
await pubsub.unsubscribe(*channels)
70+
# prune this connection
71+
await redis.zrem(stats_key, conn_id)
72+
73+
# prune stale connections (> 30s)
74+
await redis.zremrangebyscore(stats_key, 0, time.time() - 30)
75+
7376
await redis.close()
7477

7578
log.info(action='stream_close', messages_sent=total_sent_count)

backend/templates/admin/dashboard/redis_status.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
<th>Active Connections</th>
55
<td>{{ redis_stats.active_connections }}</td>
66
</tr>
7+
<tr>
8+
<th>Stream Connections</th>
9+
<td>{{ redis_stats.active_stream_users }}</td>
10+
</tr>
711
<tr>
812
<th>Usage</th>
913
<td>{{ redis_stats.usage }}</td>

0 commit comments

Comments
 (0)