65 lines
2.4 KiB
Python
65 lines
2.4 KiB
Python
"""Redis-backed live canvas state, one canvas per event.
|
|
|
|
Redis is read/written on the hot path (every placement); the DB
|
|
(PixelPlacement) is the durable history and is never queried on the hot
|
|
path. Each event's canvas is its own string key, one byte per pixel
|
|
(row-major), holding the palette index - SETRANGE self-pads with zero
|
|
bytes, so there's no separate "ensure canvas exists" bootstrap step.
|
|
|
|
The grid's width/height aren't fixed - they live on PlaceSettings (see
|
|
models.py) and an admin can change them per event - so every function here
|
|
takes them as arguments rather than reading a constant. Callers that already
|
|
have a PlaceSettings instance in hand (consumers, the resync admin action)
|
|
just pass its width/height through.
|
|
"""
|
|
|
|
import redis
|
|
from django.conf import settings
|
|
|
|
from .models import PixelPlacement
|
|
|
|
_client = redis.Redis.from_url(settings.PLACE_REDIS_URL)
|
|
|
|
|
|
def _canvas_key(event_path: str) -> str:
|
|
return f"place:{event_path}:canvas"
|
|
|
|
|
|
def read_canvas(event_path: str, width: int, height: int) -> bytes:
|
|
data = _client.get(_canvas_key(event_path))
|
|
if data is None:
|
|
return bytes(width * height)
|
|
return data
|
|
|
|
|
|
def write_pixel(event_path: str, x: int, y: int, color_index: int, width: int) -> None:
|
|
_client.setrange(_canvas_key(event_path), y * width + x, bytes([color_index]))
|
|
|
|
|
|
def rebuild_canvas_from_history(event, width: int, height: int) -> int:
|
|
"""Replays the durable PixelPlacement history for `event`, in placement
|
|
order, into a fresh canvas sized width x height and overwrites the live
|
|
Redis copy with it in one atomic SET. Used to repair Redis after it's
|
|
lost/diverged from the DB (a flush, a restart without persistence, a
|
|
resize - the old Redis bytes don't match the new dimensions - or a write
|
|
that raced another one to the same pixel and landed out of order).
|
|
Placements that fall outside the current dimensions (from a placement
|
|
made before a shrink) are skipped rather than raising. Returns the number
|
|
of placements replayed.
|
|
"""
|
|
grid = bytearray(width * height)
|
|
count = 0
|
|
placements = (
|
|
PixelPlacement.objects.filter(event=event)
|
|
.order_by("placed_at")
|
|
.values_list("x", "y", "color_index")
|
|
)
|
|
for x, y, color_index in placements.iterator():
|
|
if x >= width or y >= height:
|
|
continue
|
|
grid[y * width + x] = color_index
|
|
count += 1
|
|
|
|
_client.set(_canvas_key(event.path), bytes(grid))
|
|
return count
|