Files
polylan-submitter/polylan_submitter/place/canvas.py
T

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