Skip to content

Template Cache

The Template Cache is a three-tier caching system that stores ML priority predictions for Drain3 templates to avoid repeated RoBERTa inference.


Architecture

%%{init: {'theme':'default','themeVariables':{'primaryColor':'#1a73e8'}}}%%
graph TB
  subgraph PIPELINE["Pipeline Process"]
    L2[L2: In-Memory Dict<br/>~μs lookup]
  end

  subgraph SHARED["Shared Infrastructure"]
    L3[(L3: Redis<br/>ml:priority:{tid})]
    L1[(L1: PostgreSQL<br/>template_priority)]
  end

  EVENT[Event with template_id] --> L2
  L2 -->|Miss| L3
  L3 -->|Miss| L1
  L1 -->|Miss| ROBERTA[RoBERTa Inference]
  ROBERTA -->|Store| L1
  ROBERTA -->|Store| L3
  ROBERTA -->|Store| L2

Three Tiers

Tier Technology Scope Latency Persistence Write Policy
L1 PostgreSQL (template_priority) Global, durable ~5 ms Permanent INSERT ... ON CONFLICT
L2 Python dict (in-process) Per replica < 1 μs Process lifetime Direct dict write
L3 Redis (ml:priority:{tid}) Cluster-wide < 1 ms TTL 24h SETEX

Cache Interface

# template_cache.py
class TemplateCache:
    def __init__(self, dsn: str, redis_client, model_version: str):
        self.pg_pool = psycopg_pool.AsyncConnectionPool(dsn)
        self.redis = redis_client
        self.model_version = model_version
        self.local_cache: dict[str, CachedPriority] = {}

    async def load_from_postgres(self):
        """Load all cached priorities at startup."""
        async with self.pg_pool.connection() as conn:
            rows = await conn.execute("""
                SELECT template_id, priority, confidence, model_version
                FROM template_priority
            """)
            for row in rows:
                self.local_cache[row.template_id] = CachedPriority(...)

    def get(self, template_id: str) -> CachedPriority | None:
        # L2: In-memory
        if template_id in self.local_cache:
            return self.local_cache[template_id]

        # L3: Redis (async in real impl, sync here for simplicity)
        cached = self.redis.get(f"ml:priority:{template_id}")
        if cached:
            self.local_cache[template_id] = parse_cached(cached)
            return self.local_cache[template_id]

        return None

    def set(self, template_id: str, template_text: str, 
            priority: str, confidence: float):
        # Only cache if confidence >= threshold
        if confidence < settings.ml_min_confidence:
            return

        cached = CachedPriority(
            priority=priority,
            confidence=confidence,
            model_version=self.model_version,
        )

        # L2
        self.local_cache[template_id] = cached

        # L3: Redis (TTL 24h)
        self.redis.setex(
            f"ml:priority:{template_id}",
            86400,
            orjson.dumps(cached.model_dump())
        )

        # L1: PostgreSQL (async, fire-and-forget)
        asyncio.create_task(self._persist_to_postgres(template_id, cached))

Conditional Storage

Critical: Only cache predictions with confidence ≥ 0.80

def set(self, template_id, template_text, priority, confidence):
    if confidence < settings.ml_min_confidence:  # 0.80
        return  # Don't cache low-confidence predictions

    # ... store in all tiers

Why? Low-confidence predictions pollute cache, reduce hit rate, propagate errors.


PostgreSQL Schema

-- migration 0011
CREATE TABLE template_priority (
    template_id TEXT PRIMARY KEY REFERENCES templates(template_id),
    template_text TEXT NOT NULL,
    priority VARCHAR(2) NOT NULL CHECK (priority IN ('P1','P2','P3','P4')),
    confidence REAL NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
    model_version VARCHAR(100) NOT NULL,
    prediction_count INTEGER DEFAULT 0,
    trained_at DATE DEFAULT CURRENT_DATE,
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX ix_template_priority_priority ON template_priority (priority);

Cache Invalidation

Trigger Action
Manual priority correction (PATCH /api/logs/{id}/priority) DELETE FROM template_priority WHERE template_id = ? + redis.delete() + local_cache.pop()
Model version change Full cache clear on startup (model_version mismatch)
TTL expiry Redis auto-evicts (24h); L2 cleared on process restart
# API endpoint invalidates cache
async def update_event_priority(event_id, priority, reason):
    # ... update event ...
    if event.template_id:
        await template_cache.invalidate(event.template_id)

Metrics

Metric Target Source
Hit Rate ≥ 95% ml_cache_hits / (ml_cache_hits + ml_predictions)
Known Templates Growing SELECT COUNT(*) FROM template_priority
Avg Confidence > 0.85 AVG(confidence) FROM template_priority
Low Confidence < 5% COUNT(*) WHERE confidence < 0.80
# Via API
curl -s http://localhost:8000/api/ai-insights/stats | jq .ml_status
# "healthy" if hit_rate >= 95% and low_conf_count == 0

Troubleshooting

Issue Diagnosis Fix
Hit rate < 90% Many new templates, or cache cleared Check ml_predictions_total rate; wait for cache warmup
Stale predictions Model updated but cache not cleared Restart pipeline (clears L2), or manual redis.flushdb
PostgreSQL growing Old templates never expire Add retention job: DELETE WHERE updated_at < NOW() - INTERVAL '90 days'
Redis memory high Too many keys Reduce TTL, or add maxmemory-policy allkeys-lru
# Check cache stats
curl -s http://localhost:8000/api/ai-insights/engine | jq .ml