diff --git a/CHANGELOG.md b/CHANGELOG.md index a1da6ce0..7160ea0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### Changed +- Multi-table writes (document create, update, batch import, cascade delete) go through `Store.write_transaction()`. Rollback restores in `RESTORE_TABLE_ORDER` and is shielded from cancellation, so a cancelled write rolls back instead of committing part of itself. `Store.restore_table_versions()` is removed. - Search enrichment, the multimodal reranker's picture fetch, and context expansion each issue a fixed number of `document_items` queries regardless of how many documents a result set spans, instead of one set per document. `expand_with_items` takes the items to expand from rather than fetching them, and `DocumentItemRepository` gains `resolve_refs_grouped`, `get_items_in_ranges`, `get_pictures_grouped` and `get_caption_picture_refs_grouped`. The superseded methods are removed: `resolve_refs`, `get_items_in_range`, `get_caption_picture_refs`, `get_text_for_refs` and `get_all_items_grouped`. `get_pictures_grouped` returns each picture's text alongside its bytes under `with_text`, off by default so the reranker's blob fetch does not read a column it discards. - Eval judge pinned to `qwen3.8`: `DEFAULT_JUDGE_MODEL` is `ollama:qwen3.8`, and the reference configs use `Inferact/Qwen3.8-27B-NVFP4` with `extra_body.chat_template_kwargs.reasoning_effort: low`. Results in `docs/benchmarks.md` were judged by `Qwen3.6-35B-A3B-NVFP4` and are not re-judged. - `create_capability(rag=...)` lends a capability an open client rather than having it open its own; `client.ask`/`client.analyze` now pass theirs. diff --git a/haiku_rag_slim/haiku/rag/client/__init__.py b/haiku_rag_slim/haiku/rag/client/__init__.py index 4dd365a9..278e3627 100644 --- a/haiku_rag_slim/haiku/rag/client/__init__.py +++ b/haiku_rag_slim/haiku/rag/client/__init__.py @@ -391,7 +391,7 @@ class HaikuRAG: """ from haiku.rag.client.documents import parent_uri_filter - async with self.store._write_lock: + async with self.store.write_transaction(): # Resolve existence and collect the subtree under the lock so two # concurrent deletes of the same id can't both proceed, and children # can't appear or move between collection and deletion. parent_uri @@ -414,13 +414,8 @@ class HaikuRAG: if not ids_to_delete: return False - versions = await self.store.current_table_versions() - try: - for doc_id in ids_to_delete: - await self.document_repository.delete(doc_id) - except Exception: - await self.store.restore_table_versions(versions) - raise + for doc_id in ids_to_delete: + await self.document_repository.delete(doc_id) if self._config.storage.auto_vacuum: self._schedule_vacuum() diff --git a/haiku_rag_slim/haiku/rag/client/documents.py b/haiku_rag_slim/haiku/rag/client/documents.py index e3b23df1..d67f4f99 100644 --- a/haiku_rag_slim/haiku/rag/client/documents.py +++ b/haiku_rag_slim/haiku/rag/client/documents.py @@ -120,9 +120,7 @@ async def _store_document_with_chunks( chunks = await ensure_chunks_embedded(client._config, chunks, client.embedder) items = await asyncio.to_thread(extract_items, "", docling_document) - async with client.store._write_lock: - versions = await client.store.current_table_versions() - + async with client.store.write_transaction(): # A concurrent ingestion of the same URI may have created the document # while this one was converting/embedding outside the lock. LanceDB has # no unique constraint on `uri`, so re-check under the lock and update in @@ -133,41 +131,33 @@ async def _store_document_with_chunks( else None ) - try: - if existing is not None: - document.id = existing.id - document.created_at = existing.created_at - stored_doc = await client.document_repository.update(document) - else: - stored_doc = await client.document_repository.create(document) + if existing is not None: + document.id = existing.id + document.created_at = existing.created_at + stored_doc = await client.document_repository.update(document) + else: + stored_doc = await client.document_repository.create(document) - assert stored_doc.id is not None, ( - "Document ID should not be None after storing" + assert stored_doc.id is not None, "Document ID should not be None after storing" + for order, chunk in enumerate(chunks): + chunk.document_id = stored_doc.id + chunk.order = order + for item in items: + item.document_id = stored_doc.id + + if existing is not None: + await client.chunk_repository.replace_for_document(stored_doc.id, chunks) + await client.document_item_repository.replace_for_document( + stored_doc.id, items ) - for order, chunk in enumerate(chunks): - chunk.document_id = stored_doc.id - chunk.order = order - for item in items: - item.document_id = stored_doc.id + else: + await client.chunk_repository.create(chunks) + await client.document_item_repository.create_items(stored_doc.id, items) - if existing is not None: - await client.chunk_repository.replace_for_document( - stored_doc.id, chunks - ) - await client.document_item_repository.replace_for_document( - stored_doc.id, items - ) - else: - await client.chunk_repository.create(chunks) - await client.document_item_repository.create_items(stored_doc.id, items) + if client._config.storage.auto_vacuum: + client._schedule_vacuum() - if client._config.storage.auto_vacuum: - client._schedule_vacuum() - - return stored_doc - except Exception: - await client.store.restore_table_versions(versions) - raise + return stored_doc async def _update_document_with_chunks( @@ -200,31 +190,25 @@ async def _update_document_with_chunks( extract_items, document.id, docling_document, existing_picture_data ) - async with client.store._write_lock: - versions = await client.store.current_table_versions() + async with client.store.write_transaction(): + updated_doc = await client.document_repository.update(document) - try: - updated_doc = await client.document_repository.update(document) + assert updated_doc.id is not None + for order, chunk in enumerate(chunks): + chunk.document_id = updated_doc.id + chunk.order = order - assert updated_doc.id is not None - for order, chunk in enumerate(chunks): - chunk.document_id = updated_doc.id - chunk.order = order + await client.chunk_repository.replace_for_document(updated_doc.id, chunks) - await client.chunk_repository.replace_for_document(updated_doc.id, chunks) + if items is not None: + await client.document_item_repository.replace_for_document( + updated_doc.id, items + ) - if items is not None: - await client.document_item_repository.replace_for_document( - updated_doc.id, items - ) + if client._config.storage.auto_vacuum: + client._schedule_vacuum() - if client._config.storage.auto_vacuum: - client._schedule_vacuum() - - return updated_doc - except Exception: - await client.store.restore_table_versions(versions) - raise + return updated_doc async def create_document( @@ -315,36 +299,30 @@ async def _store_documents_with_chunks( all_item_lists = await asyncio.to_thread(_extract_all_items) - async with client.store._write_lock: - versions = await client.store.current_table_versions() - + async with client.store.write_transaction(): created = await client.document_repository.create( [doc for doc, _, _ in prepared] ) - try: - all_chunks: list[Chunk] = [] - all_items = [] - for doc, doc_chunks, item_list in zip(created, embedded, all_item_lists): - assert doc.id is not None - for order, chunk in enumerate(doc_chunks): - chunk.document_id = doc.id - chunk.order = order - all_chunks.extend(doc_chunks) - for item in item_list: - item.document_id = doc.id - all_items.extend(item_list) + all_chunks: list[Chunk] = [] + all_items = [] + for doc, doc_chunks, item_list in zip(created, embedded, all_item_lists): + assert doc.id is not None + for order, chunk in enumerate(doc_chunks): + chunk.document_id = doc.id + chunk.order = order + all_chunks.extend(doc_chunks) + for item in item_list: + item.document_id = doc.id + all_items.extend(item_list) - await client.chunk_repository.create(all_chunks) - await client.document_item_repository.create_all(all_items) + await client.chunk_repository.create(all_chunks) + await client.document_item_repository.create_all(all_items) - if client._config.storage.auto_vacuum: - client._schedule_vacuum() + if client._config.storage.auto_vacuum: + client._schedule_vacuum() - return created - except Exception: - await client.store.restore_table_versions(versions) - raise + return created async def import_documents( diff --git a/haiku_rag_slim/haiku/rag/store/engine.py b/haiku_rag_slim/haiku/rag/store/engine.py index 08867fa6..c40dbbdd 100644 --- a/haiku_rag_slim/haiku/rag/store/engine.py +++ b/haiku_rag_slim/haiku/rag/store/engine.py @@ -1,7 +1,8 @@ import asyncio import json import logging -from collections.abc import Coroutine +from collections.abc import AsyncIterator, Coroutine +from contextlib import asynccontextmanager from dataclasses import dataclass from datetime import UTC, datetime, timedelta from enum import Enum @@ -955,16 +956,37 @@ class Store: """Capture current versions of key tables for rollback using LanceDB's API.""" return {name: await table.version() for name, table in self._tables().items()} - async def restore_table_versions(self, versions: dict[str, int]) -> bool: - """Restore tables to the provided versions using LanceDB's API. + @asynccontextmanager + async def write_transaction(self) -> AsyncIterator[None]: + """Hold the write lock for a multi-table mutation, restoring every table + to its pre-mutation version if the mutation fails. + + Rollback follows RESTORE_TABLE_ORDER and a cancellation cannot interrupt + it; a cancellation absorbed during rollback is re-delivered. A rollback + that itself fails raises with the original failure as its cause. + + In-process coordination only: a writer in another process can commit + between the version snapshot and the mutation. Raises: ReadOnlyError: If the store is in read-only mode. """ self._assert_writable() - for name, table in self._tables().items(): - await table.restore(int(versions[name])) - return True + async with self._write_lock: + versions = await self.current_table_versions() + try: + yield + except BaseException as exc: + failures, cancelled = await self._rollback_to_snapshot(versions) + if failures: + raise RuntimeError( + f"Write failed ({exc!r}) and rollback failed on: " + f"{', '.join(name for name, _ in failures)}. Tables may " + "be left inconsistent." + ) from exc + if cancelled and not isinstance(exc, asyncio.CancelledError): + raise asyncio.CancelledError() + raise async def create_tag(self, name: str) -> None: """Tag the current version of every table with the given name. diff --git a/tests/cassettes/test_versioning/test_cancellation_during_rollback_is_redelivered.yaml b/tests/cassettes/test_versioning/test_cancellation_during_rollback_is_redelivered.yaml new file mode 100644 index 00000000..dabf01fe --- /dev/null +++ b/tests/cassettes/test_versioning/test_cancellation_during_rollback_is_redelivered.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '88' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - cancel in rollback + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 4 + total_tokens: 4 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_versioning/test_cancellation_mid_write_rolls_back.yaml b/tests/cassettes/test_versioning/test_cancellation_mid_write_rolls_back.yaml new file mode 100644 index 00000000..61f94cd5 --- /dev/null +++ b/tests/cassettes/test_versioning/test_cancellation_mid_write_rolls_back.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '89' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - cancelled mid-write + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 4 + total_tokens: 4 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_versioning/test_rollback_failure_keeps_the_original_cause.yaml b/tests/cassettes/test_versioning/test_rollback_failure_keeps_the_original_cause.yaml new file mode 100644 index 00000000..af3ca544 --- /dev/null +++ b/tests/cassettes/test_versioning/test_rollback_failure_keeps_the_original_cause.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '76' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - doomed + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 3 + total_tokens: 3 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/store/test_read_only.py b/tests/store/test_read_only.py index 57bd4175..7ff281b1 100644 --- a/tests/store/test_read_only.py +++ b/tests/store/test_read_only.py @@ -110,14 +110,15 @@ class TestStoreReadOnly: await store.recreate_embeddings_table() @pytest.mark.asyncio - async def test_restore_table_versions_raises_when_read_only(self, temp_db_path): - """restore_table_versions() raises ReadOnlyError when read_only=True.""" - async with Store(temp_db_path, create=True) as store: - versions = await store.current_table_versions() + async def test_write_transaction_raises_when_read_only(self, temp_db_path): + """write_transaction() raises ReadOnlyError when read_only=True.""" + async with Store(temp_db_path, create=True): + pass async with Store(temp_db_path, read_only=True) as store: with pytest.raises(ReadOnlyError): - await store.restore_table_versions(versions) + async with store.write_transaction(): + pass # pragma: no cover - entering already raised class TestDocumentRepositoryReadOnly: diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 07b64a3f..2be4c855 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -62,6 +62,109 @@ async def test_version_rollback_on_update_failure(temp_db_path): assert len(original_chunks) > 0 +@pytest.mark.vcr() +async def test_cancellation_mid_write_rolls_back(temp_db_path): + """A cancellation between two table writes must roll back, not leave the + chunks write committed without its document.""" + async with HaikuRAG(db_path=temp_db_path, create=True) as client: + orig_create = client.chunk_repository.create + + async def succeed_then_cancel(chunks): + await orig_create(chunks) + raise asyncio.CancelledError() + + client.chunk_repository.create = succeed_then_cancel + + with pytest.raises(asyncio.CancelledError): + await client.create_document(content="cancelled mid-write") + + assert await client.list_documents() == [] + assert await client.chunk_repository.list_all() == [] + + +@pytest.mark.vcr() +async def test_rollback_failure_keeps_the_original_cause(temp_db_path): + """A failed rollback must not hide what failed first.""" + async with HaikuRAG(db_path=temp_db_path, create=True) as client: + + async def failing_restore(versions, *, best_effort=False): + return [("chunks", RuntimeError("restore refused"))] + + client.store._restore_tables = failing_restore + + async def boom(chunks): + raise RuntimeError("original failure") + + client.chunk_repository.create = boom + + with pytest.raises(RuntimeError, match="rollback failed on: chunks") as excinfo: + await client.create_document(content="doomed") + + assert isinstance(excinfo.value.__cause__, RuntimeError) + assert str(excinfo.value.__cause__) == "original failure" + + +@pytest.mark.vcr() +async def test_cancellation_during_rollback_is_redelivered(temp_db_path): + """A cancellation arriving while rollback runs cannot cut it short, and is + delivered to the caller once the restore has completed.""" + async with HaikuRAG(db_path=temp_db_path, create=True) as client: + rollback_started = asyncio.Event() + rollback_finished = asyncio.Event() + + async def slow_restore(versions, *, best_effort=False): + rollback_started.set() + await asyncio.sleep(0.05) + rollback_finished.set() + return [] + + client.store._restore_tables = slow_restore + + async def boom(chunks): + raise RuntimeError("first failure") + + client.chunk_repository.create = boom + + task = asyncio.create_task(client.create_document(content="cancel in rollback")) + await rollback_started.wait() + task.cancel() + + with pytest.raises(asyncio.CancelledError): + await task + + assert rollback_finished.is_set() + + +@pytest.mark.vcr() +async def test_batch_import_rolls_back_the_documents_write(temp_db_path): + """The batch document write is inside the guarded body, so a failure after + it lands restores the documents table too.""" + from haiku.rag.client.documents import DocumentImport + from tests.store.test_document_items import _docling_doc_with_picture + + async with HaikuRAG(db_path=temp_db_path, create=True) as client: + orig_create = client.document_repository.create + + async def succeed_then_fail(documents): + await orig_create(documents) + raise RuntimeError("after the documents write") + + client.document_repository.create = succeed_then_fail + + with pytest.raises(RuntimeError, match="after the documents write"): + await client.import_documents( + [ + DocumentImport( + docling_document=_docling_doc_with_picture(), + chunks=[], + uri="test://batch-rollback", + ) + ] + ) + + assert await client.store.documents_table.count_rows() == 0 + + async def test_new_database_does_not_run_upgrades(monkeypatch, temp_db_path): def fail_if_called(*_args, **_kwargs): raise AssertionError("run_pending_upgrades should not be called for new DB")