haiku.rag/tests/test_vacuum_debounce.py
Yiorgis Gozadinos 79a63a46d4
Give one database its own session
SingleDatabaseSession owns the store, the repositories and the vacuum
machinery, so nothing above has to ask whether it has a store. The client
keeps every name callers already use.
2026-08-24 17:13:45 +03:00

124 lines
4.6 KiB
Python

import asyncio
import pytest
import haiku.rag.client.session as session_mod
from haiku.rag.client import HaikuRAG
from haiku.rag.client.documents import _refresh_doc_metadata
from haiku.rag.config import get_config
from haiku.rag.store.models.chunk import Chunk
def _docling_doc(name: str, text: str):
from docling_core.types.doc.document import DoclingDocument
from docling_core.types.doc.labels import DocItemLabel
doc = DoclingDocument(name=name)
doc.add_text(label=DocItemLabel.TEXT, text=text)
return doc
@pytest.mark.asyncio
async def test_schedule_vacuum_is_debounced(temp_db_path, monkeypatch):
"""Rapid writes within the throttle window schedule only one background
vacuum; once the interval elapses, a new one is scheduled."""
t = {"now": 1000.0}
monkeypatch.setattr(session_mod, "monotonic", lambda: t["now"])
async with HaikuRAG(temp_db_path, create=True) as client:
calls: list[int] = []
async def fake_vacuum(*_a, **_k):
calls.append(1)
monkeypatch.setattr(client.store, "vacuum", fake_vacuum)
for _ in range(3):
client._session.schedule_vacuum()
await asyncio.gather(*client._session._vacuum_tasks)
assert len(calls) == 1 # debounced within the interval
t["now"] += session_mod._VACUUM_MIN_INTERVAL_S + 1
client._session.schedule_vacuum()
await asyncio.gather(*client._session._vacuum_tasks)
assert len(calls) == 2 # interval elapsed -> a new vacuum scheduled
@pytest.mark.asyncio
async def test_debounced_writes_still_collapse_on_close(temp_db_path, monkeypatch):
"""Even when scheduled vacuums after the first are debounced, the writes are
marked dirty so the close-time drain runs a final collapse."""
t = {"now": 1000.0}
monkeypatch.setattr(session_mod, "monotonic", lambda: t["now"])
calls: list[int] = []
async with HaikuRAG(temp_db_path, create=True) as client:
async def fake_vacuum(*_a, **_k):
calls.append(1)
monkeypatch.setattr(client.store, "vacuum", fake_vacuum)
client._session.schedule_vacuum() # schedules the first background pass
client._session.schedule_vacuum() # debounced (no task)
await client._session.drain_vacuum()
# one scheduled background pass + one final collapse on drain
assert len(calls) == 2
assert client._session._vacuum_dirty is False
@pytest.mark.asyncio
async def test_metadata_refresh_sweep_schedules_vacuum(temp_db_path):
"""A source re-sweep that only rolls source_revision (MD5/revision
short-circuit) writes document_meta and must still schedule the (debounced)
vacuum, so that tiny churn gets reclaimed instead of accumulating."""
dim = get_config().embeddings.model.vector_dim
async with HaikuRAG(temp_db_path, create=True) as client:
doc = await client.import_document(
_docling_doc("d", "body"),
[Chunk(content="body", embedding=[0.1] * dim, order=0)],
uri="mem://sweep",
metadata={"source_revision": "r1"},
)
# Isolate the refresh: the import already scheduled a vacuum.
client._session._vacuum_dirty = False
await _refresh_doc_metadata(
client,
doc,
title=None,
user_metadata={},
source_metadata={"source_revision": "r2", "md5": "same"},
)
assert client._session._vacuum_dirty is True
@pytest.mark.asyncio
async def test_metadata_refresh_waits_for_write_lock(temp_db_path):
"""The revision/MD5 short-circuit write serializes with other writers so
it cannot land inside another writer's critical section (e.g. between
create_tag's version snapshot and its per-table tag creation)."""
dim = get_config().embeddings.model.vector_dim
async with HaikuRAG(temp_db_path, create=True) as client:
doc = await client.import_document(
_docling_doc("d", "body"),
[Chunk(content="body", embedding=[0.1] * dim, order=0)],
uri="mem://sweep",
metadata={"source_revision": "r1"},
)
async with client.store._write_lock:
task = asyncio.create_task(
_refresh_doc_metadata(
client,
doc,
title=None,
user_metadata={},
source_metadata={"source_revision": "r2", "md5": "same"},
)
)
await asyncio.sleep(0.1)
assert not task.done()
refreshed = await task
assert refreshed.metadata["source_revision"] == "r2"