Implementing Cache Invalidation for Real-Time Indoor Map Updates
This page covers the consumer half of Cache Invalidation Strategies for Indoor Maps: how an edge worker and a client SDK react to a map:invalidate event so a corridor closed thirty seconds ago stops appearing as a traversable edge. The linked strategies guide builds the dispatcher that publishes the event; here we build the workers that receive it, diagnose what went stale, and evict exactly the affected vector tiles, routing subgraph, and POI metadata — nothing more.
What “real-time invalidation” means here
Real-time invalidation is the bounded propagation of a single content-addressed change to every cache tier within one navigation interval (sub-second to a few seconds), driven by an explicit event rather than by TTL decay. The unit of change is a topology_hash — the deterministic SHA-256 of a floor level’s serialized routing graph, the same hash minted during validation and reused as the cache key in Rollback Triggers & Versioning. “Real-time” does not mean polling on a tight loop; it means a subscriber wakes on a published message, compares its stored hash to the expected one, and fetches only the delta when they differ. Three distinct caches must converge on the new hash:
- CDN / edge vector tiles — the rendered
z/x/yMVT a client paints. - The routing subgraph — precomputed adjacency and shortest-path data for one floor level.
- POI / occupancy metadata — the “room bookable / exit open” records that drive UI state.
The hard part is that these are three different keys with three different lifetimes. Purging the tile but not the metadata leaves markers drifting off geometry; purging the graph but not the tile routes a user through a wall that is still painted as open. The consumer’s job is to evict all three coherently, keyed on one hash.
Minimal working example: an event consumer
The smallest useful consumer subscribes to the invalidation channel, validates the payload against the established envelope, and reacts. This snippet is self-contained: run it against a local Redis and publish a test message to watch it evict.
import asyncio
import logging
from typing import Optional
import redis.asyncio as redis
from pydantic import BaseModel, ValidationError
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("invalidation_consumer")
class InvalidationEvent(BaseModel):
building_id: str
floor_level: str
map_version: str
topology_hash: str
change_type: str # TOPOLOGY_UPDATE | POI_METADATA | BEACON_RECALIBRATION
async def consume(redis_url: str, channel: str = "map:invalidate:*") -> None:
client = redis.from_url(redis_url, decode_responses=True)
pubsub = client.pubsub()
await pubsub.psubscribe(channel)
logger.info("subscribed to %s", channel)
async for message in pubsub.listen():
if message["type"] != "pmessage":
continue
try:
event = InvalidationEvent.model_validate_json(message["data"])
except ValidationError as exc:
logger.error("dropping malformed event: %s", exc)
continue
logger.info("evicting %s/%s -> %s", event.building_id,
event.floor_level, event.topology_hash[:12])
# purge_edge(event) and notify_sdk(event) fan out from here
if __name__ == "__main__":
asyncio.run(consume("redis://localhost:6379/0"))
The pattern subscription (psubscribe on map:invalidate:*) means one worker handles every building and floor without re-subscribing per zone. Validation happens before any eviction, so a corrupt message is logged and skipped rather than triggering a blind flush.
Event payload reference
The published payload mirrors the metadata block of the canonical GeoJSON FeatureCollection, so the consumer never has to learn a second schema. Keys, types, and consumer behaviour:
| Field | Type | Default | Notes |
|---|---|---|---|
building_id |
str |
— | Stable building identifier; the high-order cache-key segment. |
floor_level |
str |
— | Floor identifier (use the level token, not a render layer index). |
map_version |
str |
— | SemVer MAJOR.MINOR.PATCH; MAJOR bump signals a schema change that forces a full re-fetch. |
topology_hash |
str (64 hex) |
— | Content-addressed key; the idempotency key for every eviction. |
change_type |
enum |
TOPOLOGY_UPDATE |
Selects which tiers to purge: graph+tiles, metadata-only, or beacon zone. |
timestamp |
float |
time.time() |
Wall-clock for telemetry only — never used as a cache key. |
ttl_fallback |
int (s) |
60 |
Safety net if the event is missed; the longest a stale tier can persist. |
Two diagnostics confirm an eviction actually landed before you trust it. Use a conditional curl to prove the edge dropped the tile — a 200 (not 304) means the surrogate-key purge worked:
curl -s -o /dev/null -w "%{http_code} %{header_json}" \
-H 'If-None-Match: "<old_etag>"' \
https://tiles.indoor-map.internal/v1/BLDG-04/floor/3/12/405/230.mvt
And confirm the routing subgraph version flipped in Redis, scoped to one floor rather than dumping the keyspace:
redis-cli HGET "routing:graph:BLDG-04:floor_3" topology_hash
Common errors and fixes
1. pydantic.ValidationError: floor_level field required. A producer is still emitting the legacy floor_id key. Root cause: the dispatcher predates the GeoJSON envelope alignment. Fix at the consumer boundary with a tolerant alias rather than letting the event drop silently:
from pydantic import BaseModel, Field
class InvalidationEvent(BaseModel):
floor_level: str = Field(validation_alias="floor_id") # accept legacy key
2. Stale tiles persist after a successful graph purge (split-tier eviction). The change_type was TOPOLOGY_UPDATE but the worker only evicted the routing_graph surrogate key. Root cause: tiles and graphs are separate keys; geometry edits invalidate both. Purge them together:
def surrogate_keys(event: InvalidationEvent) -> list[str]:
b, f, h = event.building_id, event.floor_level, event.topology_hash
keys = [f"routing_graph_{b}_{f}"]
if event.change_type in ("TOPOLOGY_UPDATE",):
keys.append(f"vector_tiles_{b}_{f}_{h}") # tiles share the floor edit
if event.change_type in ("TOPOLOGY_UPDATE", "POI_METADATA"):
keys.append(f"poi_meta_{b}_{f}")
logger.info("purging %d surrogate keys for %s/%s", len(keys), b, f)
return keys
3. redis.exceptions.ConnectionError mid-stream leaves a client serving the old graph. A network partition dropped the subscriber; plain Pub/Sub is fire-and-forget, so the missed message is gone. Root cause: at-most-once delivery. Fix by reconciling on reconnect — compare the locally stored hash to the origin and re-evict if they disagree — and, for stronger guarantees, move to Redis Streams with XREADGROUP so the missed entry is redelivered:
async def reconcile_on_reconnect(client, building: str, floor: str, origin_hash: str) -> Optional[str]:
try:
local = await client.hget(f"routing:graph:{building}:{floor}", "topology_hash")
except redis.ConnectionError as exc:
logger.error("reconcile failed, holding last-known-good: %s", exc)
return None
if local != origin_hash:
logger.warning("hash drift on %s/%s, forcing re-evict", building, floor)
return origin_hash # caller re-runs the purge + delta fetch
return None
How this feeds the rest of the deployment
This consumer sits directly downstream of the dispatcher described in Cache Invalidation Strategies for Indoor Maps and only ever fires for artifacts that already passed CI Gating for Map Updates and the JSON Schema Design for Indoor Maps contract — invalidating toward a malformed graph is worse than serving a stale-but-valid one. The delta the SDK fetches is consumed through the client patterns in SDK Integration Patterns; when three consecutive syncs fail, the worker emits a CACHE_DEGRADATION metric and the engine drops onto the graceful-degradation ladder defined by Fallback Routing Architectures, finishing the user’s in-progress route on the last-known-good graph and reconciling at the next decision point.
Related pages
- Cache Invalidation Strategies for Indoor Maps — the dispatcher side that publishes the events this page consumes.
- Designing JSON Schemas for Indoor Map APIs — the payload contract that defines the metadata envelope used as the cache key.
- Integrating Indoor Maps with React Native SDKs — how the client applies the delta fetched after an invalidation.
This page is part of the Cache Invalidation Strategies cluster within the Production-Ready Indoor Map Deployment reference.