Un-key stage-cache rows written before the current key shape

CACHE_SHAPE_SINCE marks the last change that moved every cache key. The
run service's periodic sweep clears cache_key on rows of runs created
before it, which nothing can match any more. Their outputs stay.
This commit is contained in:
2026-10-15 10:54:22 +02:00
parent 6fa44bacf3
commit a95be36dba
5 changed files with 69 additions and 4 deletions
+2 -1
View File
@@ -1216,7 +1216,8 @@ class FlowController:
# reaches hash to, which `sync` worked out by following
# its imports. The ports are here because declaring a new
# one changes what the node answers with, which a hit
# would otherwise restore without.
# would otherwise restore without. Changing what goes in
# here changes every key: move `CACHE_SHAPE_SINCE` with it.
node.fingerprint = hashlib.sha256(
json.dumps(
{
+8
View File
@@ -23,6 +23,7 @@ from collections import deque
from collections.abc import Callable, Iterator
from concurrent.futures import Future, ThreadPoolExecutor, wait
from contextlib import contextmanager
from datetime import UTC, datetime
from functools import lru_cache
from typing import Any, Literal, Protocol
@@ -166,6 +167,13 @@ class RunCacheLookup(Protocol):
"""What an equal execution produced, or None when there is no entry."""
#: When the current shape of a cache key landed: the last change to what a
#: fingerprint hashes (`FlowController`) or to how the key below reduces its
#: inputs that moved every key. A row written before it matches nothing now,
#: so the run service's sweep un-keys it. Move it forward with such a change.
CACHE_SHAPE_SINCE = datetime(2026, 8, 26, 19, 27, 10, tzinfo=UTC)
def run_cache_key(fingerprint: str, inputs: dict[str, Any], flow: str = "") -> str:
"""What this node, with these inputs, is known by.
+22 -1
View File
@@ -52,7 +52,7 @@ from fluksio.core.db import engine as db_engine
from fluksio.flow.artifacts import ArtifactStore, is_reference, valid_digest
from fluksio.flow.controller import NODE_TYPES, FlowController, RunContext
from fluksio.flow.messages import qualify
from fluksio.flow.pipeline import CacheHit, NodeOutcome, Pipeline
from fluksio.flow.pipeline import CACHE_SHAPE_SINCE, CacheHit, NodeOutcome, Pipeline
from fluksio.flow.queue import WorkItem, WorkQueue
from fluksio.flow.schemas import FlowDef, NodeDef
from fluksio.flow.state import MemoryState, StateBackend
@@ -759,6 +759,26 @@ def _cacheable(outcome: NodeOutcome) -> str | None:
return text if len(text) <= OUTPUT_CAP else None
def prune_stale_cache() -> int:
"""Un-key the stage-cache rows written before the current key shape.
Such a row can match no key a node computes any more, so it is a cache
entry in name only. The row and its outputs stay: they are the record of
what that run's node returned. Returns how many rows were un-keyed.
"""
with Session(db_engine) as session:
older = select(Run.id).where(col(Run.created_at) < CACHE_SHAPE_SINCE)
result = session.exec(
update(RunNode)
.where(col(RunNode.cache_key) != "", col(RunNode.run_id).in_(older))
.values(cache_key="")
)
session.commit()
if result.rowcount:
logger.info("Un-keyed %d stage-cache rows of an older shape", result.rowcount)
return int(result.rowcount)
class RunCache:
"""What earlier runs already worked out, looked up by cache key.
@@ -1353,6 +1373,7 @@ class RunService:
if time.monotonic() - last_sweep >= SWEEP_INTERVAL_S:
last_sweep = time.monotonic()
self._sweep_abandoned(now)
prune_stale_cache()
except Exception:
logger.exception("Could not refresh run leases")