RBAC cache — invalidace je no-op, když klíč dostal prefix a delete ne
import { Aside } from ‘@astrojs/starlight/components’;
Symptom
Sekce “Symptom”Admin odebere uživateli právo (nebo změní scope práva role) — a uživatel s právem dál normálně pracuje ještě až 5 minut. Žádná chyba v logu; po chvíli se to „samo spraví” (vyprší TTL cache), takže se bug dlouho tváří jako flaky chování, ne jako díra v invalidaci.
Root cause
Sekce “Root cause”In-memory permission cache dostala při přechodu na multi-tenant tenant prefix v klíči, ale cílená invalidace zůstala z single-tenant éry:
// zápis do cache — klíč S prefixemconst cacheKey = `${tenantCacheKey()}:${userId}`;userPermissionCache.set(cacheKey, map);
// invalidace — klíč BEZ prefixu → nikdy nic nesmažeexport function clearUserPermissionCache(userId: string) { userPermissionCache.delete(userId); // no-op!}Map.delete() s neexistujícím klíčem tiše vrátí false — nic nespadne,
typecheck projde, testy „cache funguje” projdou. Jediné, co zmizelo, je
efekt invalidace. Stejný vzor postihl i rolovou cache (delete(role) vs.
klíč tenant:role) a jedno místo invalidaci nemělo vůbec (smazání role).
Druhá vrstva téhož problému: in-memory invalidace zafunguje jen v procesu,
který právo změnil. Práva ale mění i jiné procesy (seed skripty, CLI,
případný PM2 cluster) — tam žádný Map.delete() běžící aplikaci nedoběhne.
Fix
Sekce “Fix”- Tenant-aware mazání podle suffixu — iterace Map je levná a over-invalidace bezpečná (stojí jeden SELECT navíc):
export function clearUserPermissionCache(userId: string) { const suffix = `:${userId}`; for (const key of userPermissionCache.keys()) { if (key.endsWith(suffix)) userPermissionCache.delete(key); }}Mazání „s aktuálním tenant prefixem” je past — zapisovatel může běžet
pod jinou podobou klíče (slug fallback vs. dbName, CLI/master-admin
kontext) a miss = tichý návrat bugu. Suffix obsahuje oddělovač :,
takže :SALES netrefí :PRESALES.
- Cross-process verzovací klíč v DB — key-value řádek
rbac_cache_versionv tenant DB. Každý zapisovatel práv (API routy i seed skripty) po zápisu verzi bumpne; čtenář ji throttlovaně ověří při přístupu do cache (1 indexovaný SELECT / ~10 s / tenant / proces) a při změně zahodí cache celého tenanta. Efekt: vlastní proces okamžitě, ostatní procesy do ~10 s místo 5 min. Bez nové tabulky a migrace.
Jak se tomu vyvarovat v jiných systémech
Sekce “Jak se tomu vyvarovat v jiných systémech”- Detection: grepni každou
new Map()cache na páryset(/delete(a porovnej, jak se klíč skládá — jakýkoli rozdíl v konstrukci klíče mezi zápisem a mazáním = bug. Code review otázka: „kdo všechno tenhle klíč maže a skládá ho stejně?” - Anti-pattern: klíč se skládá inline template stringem na víc místech; invalidace přijímá „logické id” (userId), zatímco cache klíčuje kompozitem (tenant:userId). TTL maskuje rozbitou invalidaci.
- Lepší přístup: jediná funkce pro tvorbu klíče + invalidace mazáním podle suffixu/prefixu; deterministický test okamžitého grant/revoke (změna v DB + invalidace → hned účinná, bez čekání na TTL); u více procesů (cluster, seed skripty) verzovací klíč v DB kontrolovaný při cache-hitu, ne spoléhání na in-memory delete.
Sister bugs / související
Sekce “Sister bugs / související”- Multi-tenant — module-level in-memory cache leakuje data mezi tenanty — původní oprava, která prefix do klíčů zavedla; tenhle bug je její nedotažený druhý krok (čtení dostalo prefix, invalidace ne).
- Read-after-write přes TTL cache — příbuzná past: TTL cache maskuje chybějící/rozbitou invalidaci.