* perf(session-load): memoize the compression-lineage display stitch _webui_sidecar_lineage_messages_for_display re-merged multi-thousand-row parent+child transcripts on EVERY GET /api/session (measured 5.1s per request on a real 5.2k-message lineage, py-spy: merge_session_messages_ append_only + _matching_visible_duplicate dominate). The merge result is now cached per session id, validity keyed by the exact stat signature of the child sidecar AND every snapshot parent involved, bounded LRU(16). Any write to any involved sidecar invalidates naturally; cache hits and the populating call hand out shallow-copied rows so caller-side display metadata cannot corrupt cached state. Measured: 5.17s cold -> 0.002s warm on the real session; invalidation after touch re-merges. Tests: tests/test_lineage_display_cache.py (correctness, copy isolation, child/parent invalidation, no pollution for lineage-less sessions). * perf(api): memoize inactive-session display merges * perf(api): skip state load on cached transcript reopen * test(api): remove unused lineage-cache import * fix(api): reject streaming key without state database * test(api): isolate display cache fixture * fix(api): bind cache publication to read signature * fix(api): harden display cache invalidation * fix(api): bind cache hits to lineage provenance * fix(api): close remaining display cache scope races * fix(api): scope display cache revision to target * fix(api): evict unverifiable lineage provenance * fix(api): keep incomplete lineage provenance uncached --------- Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com>
354 lines
13 KiB
Python
354 lines
13 KiB
Python
"""Coverage for skipping the state.db load on a display-merge cache hit.
|
|
|
|
GET /api/session used to materialise every state.db row for a session even when
|
|
the merged transcript was already memoized -- ~2.3s of pure waste on a 36k-row
|
|
session. The rows are only consumed by the merge, so a cache hit can skip the
|
|
load entirely.
|
|
|
|
That is only sound because the cache key no longer depends on the loaded rows.
|
|
These tests pin the two properties that make the shortcut safe:
|
|
1. a hit returns EXACTLY what the full load+merge path returns;
|
|
2. anything uncertain (active session, unbuildable key, changed data) falls
|
|
back to the full load rather than serving a stale transcript.
|
|
"""
|
|
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
from api import routes # noqa: E402
|
|
|
|
SID = "20260301_090000_cache1"
|
|
|
|
|
|
class _Session:
|
|
"""Minimal session stand-in for the cache-probe contract."""
|
|
|
|
def __init__(self, sid=SID, active=None, pending=None):
|
|
self.session_id = sid
|
|
self.profile = None
|
|
self.active_stream_id = active
|
|
self.pending_user_message = pending
|
|
self.truncation_watermark = None
|
|
self.truncation_boundary = None
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_cache():
|
|
with routes._display_merge_cache_lock:
|
|
routes._display_merge_cache.clear()
|
|
yield
|
|
with routes._display_merge_cache_lock:
|
|
routes._display_merge_cache.clear()
|
|
|
|
|
|
@pytest.fixture()
|
|
def stable_key(monkeypatch):
|
|
"""Make the cache key deterministic without touching the filesystem."""
|
|
monkeypatch.setattr(
|
|
"api.models._sidecar_stat_signature", lambda p: ("sig", 1, 2, 3), raising=False)
|
|
monkeypatch.setattr(routes, "_state_db_session_signature", lambda *a, **k: "SIG-A")
|
|
return "SIG-A"
|
|
|
|
|
|
def _seed(session, sidecar, merged):
|
|
key = routes._display_merge_cache_key(session, sidecar, None)
|
|
assert key is not None
|
|
with routes._display_merge_cache_lock:
|
|
routes._display_merge_cache[session.session_id] = {
|
|
"key": key,
|
|
"messages": merged,
|
|
"stored_at": time.monotonic(),
|
|
}
|
|
return key
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# The shortcut must be exact.
|
|
# --------------------------------------------------------------------------
|
|
|
|
def test_cache_hit_returns_the_memoized_transcript(stable_key):
|
|
session = _Session()
|
|
sidecar = [{"role": "user", "content": "hi", "timestamp": 10.0}]
|
|
merged = [
|
|
{"role": "user", "content": "hi", "timestamp": 10.0},
|
|
{"role": "assistant", "content": "hello", "timestamp": 11.0},
|
|
]
|
|
_seed(session, sidecar, merged)
|
|
|
|
got = routes._display_merge_cached_messages(session, sidecar)
|
|
assert got == merged
|
|
|
|
|
|
def test_cache_hit_returns_copies_not_shared_rows(stable_key):
|
|
"""Callers attach display metadata; the cached rows must not be mutated."""
|
|
session = _Session()
|
|
sidecar = [{"role": "user", "content": "hi", "timestamp": 10.0}]
|
|
merged = [{"role": "user", "content": "hi", "timestamp": 10.0}]
|
|
_seed(session, sidecar, merged)
|
|
|
|
got = routes._display_merge_cached_messages(session, sidecar)
|
|
got[0]["injected"] = True
|
|
|
|
again = routes._display_merge_cached_messages(session, sidecar)
|
|
assert "injected" not in again[0], "cache entry was mutated by a caller"
|
|
|
|
|
|
def test_shortcut_matches_the_full_merge_path(monkeypatch, stable_key):
|
|
"""The whole point: identical output with and without the shortcut.
|
|
|
|
Populate the cache through the real merge helper, then assert the probe
|
|
returns the same transcript the helper would have recomputed.
|
|
"""
|
|
session = _Session()
|
|
sidecar = [{"role": "user", "content": "one", "timestamp": 1.0}]
|
|
rows = [
|
|
{"role": "user", "content": "one", "timestamp": 1.0},
|
|
{"role": "assistant", "content": "two", "timestamp": 2.0},
|
|
]
|
|
|
|
full = routes._limited_webui_messages_for_display_with_sidecar(
|
|
session,
|
|
sidecar,
|
|
rows,
|
|
state_db_signature=stable_key,
|
|
)
|
|
probed = routes._display_merge_cached_messages(session, sidecar)
|
|
|
|
assert probed is not None, "merge helper did not populate the cache"
|
|
assert probed == full
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Fail-closed: never serve a stale transcript.
|
|
# --------------------------------------------------------------------------
|
|
|
|
def test_active_stream_never_uses_the_cache(stable_key):
|
|
"""An active session's in-memory tail can be ahead of its disk signature."""
|
|
session = _Session()
|
|
sidecar = [{"role": "user", "content": "hi", "timestamp": 10.0}]
|
|
_seed(session, sidecar, [{"role": "user", "content": "hi", "timestamp": 10.0}])
|
|
|
|
session.active_stream_id = "stream-123"
|
|
assert routes._display_merge_cached_messages(session, sidecar) is None
|
|
|
|
|
|
def test_pending_user_message_never_uses_the_cache(stable_key):
|
|
session = _Session()
|
|
sidecar = [{"role": "user", "content": "hi", "timestamp": 10.0}]
|
|
_seed(session, sidecar, [{"role": "user", "content": "hi", "timestamp": 10.0}])
|
|
|
|
session.pending_user_message = {"content": "not yet on disk"}
|
|
assert routes._display_merge_cached_messages(session, sidecar) is None
|
|
|
|
|
|
def test_live_memory_session_blocks_hit_from_stale_idle_object(stable_key):
|
|
"""A stale disk object must not bypass the canonical in-memory active queue."""
|
|
stale = _Session()
|
|
sidecar = [{"role": "user", "content": "hi", "timestamp": 10.0}]
|
|
merged = [{"role": "user", "content": "cached", "timestamp": 10.0}]
|
|
_seed(stale, sidecar, merged)
|
|
live = _Session(active="stream-live")
|
|
|
|
with routes.LOCK:
|
|
previous = routes.SESSIONS.get(stale.session_id)
|
|
routes.SESSIONS[stale.session_id] = live
|
|
try:
|
|
assert routes._display_merge_cached_messages(stale, sidecar) is None
|
|
finally:
|
|
with routes.LOCK:
|
|
if previous is None:
|
|
routes.SESSIONS.pop(stale.session_id, None)
|
|
else:
|
|
routes.SESSIONS[stale.session_id] = previous
|
|
|
|
|
|
def test_changed_state_db_signature_misses(monkeypatch, stable_key):
|
|
"""New state.db rows must invalidate the entry, not be silently skipped."""
|
|
session = _Session()
|
|
sidecar = [{"role": "user", "content": "hi", "timestamp": 10.0}]
|
|
_seed(session, sidecar, [{"role": "user", "content": "hi", "timestamp": 10.0}])
|
|
|
|
monkeypatch.setattr(routes, "_state_db_session_signature", lambda *a, **k: "SIG-B")
|
|
assert routes._display_merge_cached_messages(session, sidecar) is None
|
|
|
|
|
|
def test_streaming_freeze_key_hits_before_ttl(monkeypatch, stable_key):
|
|
marker = ("streaming", ("run-1",))
|
|
monkeypatch.setattr(routes, "_state_db_session_signature", lambda *a, **k: marker)
|
|
now = [1000.0]
|
|
monkeypatch.setattr(routes.time, "monotonic", lambda: now[0])
|
|
session = _Session()
|
|
sidecar = [{"role": "user", "content": "hi", "timestamp": 10.0}]
|
|
merged = [{"role": "user", "content": "hi", "timestamp": 10.0}]
|
|
_seed(session, sidecar, merged)
|
|
|
|
now[0] += routes._DISPLAY_MERGE_STREAMING_TTL_SECONDS - 0.1
|
|
assert routes._display_merge_cached_messages(session, sidecar) == merged
|
|
|
|
|
|
def test_streaming_freeze_key_expires_after_ttl(monkeypatch, stable_key):
|
|
marker = ("streaming", ("run-1",))
|
|
monkeypatch.setattr(routes, "_state_db_session_signature", lambda *a, **k: marker)
|
|
now = [1000.0]
|
|
monkeypatch.setattr(routes.time, "monotonic", lambda: now[0])
|
|
session = _Session()
|
|
sidecar = [{"role": "user", "content": "hi", "timestamp": 10.0}]
|
|
_seed(session, sidecar, [{"role": "user", "content": "hi", "timestamp": 10.0}])
|
|
|
|
now[0] += routes._DISPLAY_MERGE_STREAMING_TTL_SECONDS + 0.1
|
|
assert routes._display_merge_cached_messages(session, sidecar) is None
|
|
|
|
|
|
def test_changed_sidecar_tail_misses(stable_key):
|
|
session = _Session()
|
|
sidecar = [{"role": "user", "content": "hi", "timestamp": 10.0}]
|
|
_seed(session, sidecar, [{"role": "user", "content": "hi", "timestamp": 10.0}])
|
|
|
|
grown = sidecar + [{"role": "assistant", "content": "new", "timestamp": 12.0}]
|
|
assert routes._display_merge_cached_messages(session, grown) is None
|
|
|
|
|
|
def test_empty_cache_misses(stable_key):
|
|
assert routes._display_merge_cached_messages(_Session(), []) is None
|
|
|
|
|
|
def test_msg_before_pagination_never_uses_tail_cache(stable_key):
|
|
session = _Session()
|
|
sidecar = [{"role": "user", "content": "hi", "timestamp": 10.0}]
|
|
_seed(session, sidecar, [{"role": "user", "content": "cached tail", "timestamp": 10.0}])
|
|
|
|
assert routes._display_merge_cached_messages(
|
|
session,
|
|
sidecar,
|
|
msg_before=100,
|
|
) is None
|
|
|
|
|
|
def test_msg_before_full_read_cannot_hit_inner_tail_cache(stable_key):
|
|
"""The merge helper must not reuse the initial backstop result after paging."""
|
|
session = _Session()
|
|
tail_rows = [
|
|
{"role": "user", "content": "row-2", "timestamp": 2.0},
|
|
{"role": "assistant", "content": "row-3", "timestamp": 3.0},
|
|
]
|
|
full_rows = [
|
|
{"role": "assistant", "content": "row-1", "timestamp": 1.0},
|
|
*tail_rows,
|
|
]
|
|
|
|
initial = routes._limited_webui_messages_for_display_with_sidecar(
|
|
session,
|
|
[],
|
|
tail_rows,
|
|
state_db_signature=stable_key,
|
|
)
|
|
assert [row["content"] for row in initial] == ["row-2", "row-3"]
|
|
|
|
paging = routes._limited_webui_messages_for_display_with_sidecar(
|
|
session,
|
|
[],
|
|
full_rows,
|
|
state_db_signature=stable_key,
|
|
msg_before=2,
|
|
)
|
|
assert [row["content"] for row in paging] == ["row-1", "row-2", "row-3"]
|
|
|
|
|
|
def test_probe_fails_closed_when_bounded_signature_unavailable(monkeypatch):
|
|
"""Without the bounded signature the key would need the rows we skipped.
|
|
|
|
Fingerprinting the absent rows would key on an empty row set and could match
|
|
an entry built from real rows -- so the probe must refuse instead.
|
|
"""
|
|
monkeypatch.setattr(
|
|
"api.models._sidecar_stat_signature", lambda p: ("sig", 1, 2, 3), raising=False)
|
|
monkeypatch.setattr(routes, "_state_db_session_signature", lambda *a, **k: "SIG-A")
|
|
|
|
session = _Session()
|
|
sidecar = [{"role": "user", "content": "hi", "timestamp": 10.0}]
|
|
_seed(session, sidecar, [{"role": "user", "content": "hi", "timestamp": 10.0}])
|
|
|
|
monkeypatch.setattr(routes, "_state_db_session_signature", lambda *a, **k: None)
|
|
assert routes._display_merge_cached_messages(session, sidecar) is None
|
|
|
|
|
|
def test_cache_key_refuses_none_rows_without_bounded_signature(monkeypatch):
|
|
"""Direct contract test for the probe-path guard in the key builder."""
|
|
monkeypatch.setattr(
|
|
"api.models._sidecar_stat_signature", lambda p: ("sig", 1, 2, 3), raising=False)
|
|
monkeypatch.setattr(routes, "_state_db_session_signature", lambda *a, **k: None)
|
|
|
|
key = routes._display_merge_cache_key(_Session(), [{"timestamp": 1.0}], None)
|
|
assert key is None
|
|
|
|
|
|
def test_unsafe_session_id_is_refused(stable_key):
|
|
bad = _Session(sid="../../etc/passwd")
|
|
assert routes._display_merge_cached_messages(bad, []) is None
|
|
|
|
|
|
def test_loader_refuses_signature_when_commit_occurs_during_read(monkeypatch):
|
|
signatures = iter(("SIG-A", "SIG-B"))
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"_state_db_session_signature",
|
|
lambda *args, **kwargs: next(signatures),
|
|
)
|
|
rows = [{"role": "assistant", "content": "snapshot"}]
|
|
monkeypatch.setattr(routes, "get_state_db_session_messages", lambda *args, **kwargs: rows)
|
|
|
|
loaded, stable = routes._load_state_db_messages_with_stable_signature(
|
|
SID,
|
|
profile=None,
|
|
reader_kwargs={},
|
|
)
|
|
|
|
assert loaded == rows
|
|
assert stable is None
|
|
|
|
|
|
def test_loader_returns_signature_when_read_window_is_stable(monkeypatch):
|
|
monkeypatch.setattr(routes, "_state_db_session_signature", lambda *args, **kwargs: "SIG-A")
|
|
rows = [{"role": "assistant", "content": "snapshot"}]
|
|
monkeypatch.setattr(routes, "get_state_db_session_messages", lambda *args, **kwargs: rows)
|
|
|
|
loaded, stable = routes._load_state_db_messages_with_stable_signature(
|
|
SID,
|
|
profile=None,
|
|
reader_kwargs={},
|
|
)
|
|
|
|
assert loaded == rows
|
|
assert stable == "SIG-A"
|
|
|
|
|
|
def test_merge_does_not_publish_when_signature_changes_before_store(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"api.models._sidecar_stat_signature", lambda path: ("sig", 1, 2, 3), raising=False
|
|
)
|
|
signatures = iter(("SIG-A", "SIG-B"))
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"_state_db_session_signature",
|
|
lambda *args, **kwargs: next(signatures),
|
|
)
|
|
session = _Session()
|
|
sidecar = [{"role": "user", "content": "one", "timestamp": 1.0}]
|
|
rows = [{"role": "assistant", "content": "two", "timestamp": 2.0}]
|
|
|
|
merged = routes._limited_webui_messages_for_display_with_sidecar(
|
|
session,
|
|
sidecar,
|
|
rows,
|
|
state_db_signature="SIG-A",
|
|
)
|
|
|
|
assert len(merged) == 2
|
|
with routes._display_merge_cache_lock:
|
|
assert session.session_id not in routes._display_merge_cache
|