Ctrl K
ComunidadGuías…

test/knowledge/test_sqlite_atomic_append.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Copyright (c) 2026, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
#
# SPDX-License-Identifier: Apache-2.0

import asyncio
import sqlite3
from pathlib import Path

import pytest

pytest.importorskip("watchdog")

from ag2.knowledge import SqliteKnowledgeStore

# How long the contending appends get to reach their read before the writer lock is released.
_CONTENTION_WINDOW_S = 0.2


@pytest.mark.asyncio
@pytest.mark.parametrize("initial", ["", "existing\n"])
async def test_append_is_atomic_across_store_instances(tmp_path: Path, initial: str) -> None:
    database = tmp_path / "knowledge.sqlite"
    first = SqliteKnowledgeStore(database)
    second = SqliteKnowledgeStore(database)
    await first.write("/log.jsonl", initial)
    # Open both connections (and finish schema setup) before contention starts.
    await second.read("/log.jsonl")

    # Hold the database's writer lock while both stores try to append. An append that
    # reads before taking that lock sees the same stale content in both stores.
    blocker = sqlite3.connect(database)
    blocker.execute("BEGIN IMMEDIATE")
    payloads = ["alpha\n", "\u4e2d\u6587\n"]
    appends = [asyncio.create_task(store.append("/log.jsonl", text)) for store, text in zip((first, second), payloads)]

    try:
        # Give both appends time to reach the point where they would have read.
        await asyncio.sleep(_CONTENTION_WINDOW_S)
        blocker.rollback()
        offsets = await asyncio.gather(*appends)
        assert await first.read("/log.jsonl") in (
            initial + payloads[0] + payloads[1],
            initial + payloads[1] + payloads[0],
        )
        assert len(set(offsets)) == len(offsets)
        for offset, text in zip(offsets, payloads):
            assert await first.read_range("/log.jsonl", offset, offset + len(text.encode("utf-8"))) == text
    finally:
        blocker.rollback()
        await asyncio.gather(*appends, return_exceptions=True)
        first.close()
        second.close()
        blocker.close()


@pytest.mark.asyncio
async def test_concurrent_appends_from_many_stores_are_all_kept(tmp_path: Path) -> None:
    database = tmp_path / "knowledge.sqlite"
    stores = [SqliteKnowledgeStore(database) for _ in range(8)]
    lines = [f"line-{i}\n" for i in range(len(stores))]
    try:
        await stores[0].write("/log.jsonl", "")
        offsets = await asyncio.gather(*(store.append("/log.jsonl", line) for store, line in zip(stores, lines)))

        content = await stores[0].read("/log.jsonl")
        assert content is not None
        assert sorted(content.splitlines(keepends=True)) == lines
        assert sorted(offsets) == [i * len(lines[0]) for i in range(len(lines))]
    finally:
        for store in stores:
            store.close()


@pytest.mark.asyncio
async def test_failed_append_rolls_back_and_allows_retry(tmp_path: Path) -> None:
    database = tmp_path / "knowledge.sqlite"
    store = SqliteKnowledgeStore(database)
    await store.write("/log.jsonl", "before\n")
    with sqlite3.connect(database) as connection:
        connection.execute(
            "CREATE TRIGGER reject_append BEFORE INSERT ON entries "
            "WHEN instr(CAST(NEW.content AS TEXT), 'reject') > 0 "
            "BEGIN SELECT RAISE(ABORT, 'rejected append'); END"
        )
    try:
        with pytest.raises(sqlite3.IntegrityError, match="rejected append"):
            await store.append("/log.jsonl", "reject\n")
        assert await store.read("/log.jsonl") == "before\n"
        assert await store.append("/log.jsonl", "after\n") == len(b"before\n")
        assert await store.read("/log.jsonl") == "before\nafter\n"
    finally:
        store.close()