Files
wehub-resource-sync 85742ab165
CPU Test / Lint - next (push) Waiting to run
Dashboard / Chromatic (push) Waiting to run
CPU Test / Lint - fast (push) Waiting to run
CPU Test / Build documentation (push) Waiting to run
CPU Test / Test (Store, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Weave, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Others, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Store, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Utilities, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Weave, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Others, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Store, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Utilities, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Weave, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Others, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Store, latest, Python 3.13) (push) Waiting to run
CPU Test / Lint - slow (push) Waiting to run
CPU Test / Lint - JavaScript (push) Waiting to run
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Others, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Utilities, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Weave, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (JavaScript) (push) Waiting to run
Deploy Documentation / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:44:17 +08:00

1462 lines
52 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import asyncio
import threading
import time
from typing import (
TYPE_CHECKING,
Any,
Awaitable,
Callable,
Dict,
Iterable,
List,
Literal,
Mapping,
Sequence,
Tuple,
Union,
cast,
)
from uuid import uuid4
import pydantic
import pytest
from pydantic import BaseModel
import agentlightning.store.collection.memory as memory_module
from agentlightning.store.collection import DequeBasedQueue, DictBasedKeyValue, KeyValue, ListBasedCollection
from agentlightning.store.collection.base import Collection
from agentlightning.store.collection.memory import _item_matches_filters # pyright: ignore[reportPrivateUsage]
from agentlightning.store.collection.memory import _LoopAwareAsyncLock # pyright: ignore[reportPrivateUsage]
from agentlightning.store.collection.memory import _ThreadSafeAsyncLock # pyright: ignore[reportPrivateUsage]
from agentlightning.types import Rollout
from tests.store.conftest import QueueItem, SampleItem, mongo_client_kwargs, mongo_uri
if TYPE_CHECKING:
from pymongo.asynchronous.database import AsyncDatabase
from agentlightning.store.collection.mongo import MongoLightningCollections
pytestmark = [pytest.mark.store]
def _build_collection(items: Iterable[SampleItem] = ()) -> ListBasedCollection[SampleItem]:
return ListBasedCollection(list(items), SampleItem, ("partition", "index"))
BASE_KEY_ORDER: List[Tuple[str, int]] = [
("alpha", 1),
("alpha", 2),
("alpha", 3),
("beta", 1),
("beta", 2),
("gamma", 1),
("gamma", 2),
("delta", 1),
]
def _key_pairs(items: Sequence[SampleItem]) -> List[Tuple[str, int]]:
return [(item.partition, item.index) for item in items]
def _sorted_pairs(items: Sequence[SampleItem]) -> List[Tuple[str, int]]:
return sorted(_key_pairs(items))
def test_list_collection_requires_primary_keys(sample_items: Sequence[SampleItem]) -> None:
with pytest.raises(ValueError):
ListBasedCollection(list(sample_items), SampleItem, ())
@pytest.mark.asyncio()
async def test_list_collection_primary_keys(sample_collection: Collection[SampleItem]) -> None:
assert tuple(sample_collection.primary_keys()) == ("partition", "index")
@pytest.mark.asyncio()
async def test_list_collection_item_type(sample_collection: Collection[SampleItem]) -> None:
assert sample_collection.item_type() is SampleItem
@pytest.mark.asyncio()
async def test_list_collection_initial_size(
sample_collection: Collection[SampleItem], sample_items: Sequence[SampleItem]
) -> None:
assert (await sample_collection.size()) == len(sample_items)
@pytest.mark.asyncio()
async def test_list_collection_repr_contains_model_info(sample_collection: Collection[SampleItem]) -> None:
result = repr(sample_collection)
assert sample_collection.__class__.__name__ in result
assert "SampleItem" in result
if isinstance(sample_collection, ListBasedCollection):
assert str(await sample_collection.size()) in result
@pytest.mark.asyncio()
async def test_list_collection_insert_adds_item(sample_collection: Collection[SampleItem]) -> None:
new_item = SampleItem(partition="omega", index=1, name="omega", status="new")
await sample_collection.insert([new_item])
assert (await sample_collection.size()) == 9
result = await sample_collection.get({"partition": {"exact": "omega"}, "index": {"exact": 1}})
assert result == new_item
@pytest.mark.asyncio()
async def test_list_collection_insert_duplicate_raises(sample_collection: Collection[SampleItem]) -> None:
duplicate = SampleItem(partition="alpha", index=1, name="dup", status="new")
with pytest.raises(ValueError):
await sample_collection.insert([duplicate])
@pytest.mark.asyncio()
async def test_list_collection_insert_rejects_duplicate_payload(sample_collection: Collection[SampleItem]) -> None:
"""Ensure duplicate items within the same insert batch are rejected."""
starting_size = await sample_collection.size()
dup_a = SampleItem(partition="omega", index=1, name="dup-a", status="new")
dup_b = SampleItem(partition="omega", index=1, name="dup-b", status="new")
with pytest.raises(ValueError, match=r"(duplicated|Duplicated) primary key"):
await sample_collection.insert([dup_a, dup_b])
if isinstance(sample_collection, ListBasedCollection):
# Only ListBasedCollection supports this rejecting duplicate items within the same insert batch.
assert await sample_collection.size() == starting_size
assert await sample_collection.get({"partition": {"exact": "omega"}}) is None
@pytest.mark.asyncio()
async def test_list_collection_insert_wrong_type(sample_collection: Collection[SampleItem]) -> None:
class Another(BaseModel):
partition: str
index: int
wrong = Another(partition="omega", index=5)
with pytest.raises(TypeError):
await sample_collection.insert([wrong]) # type: ignore[arg-type]
@pytest.mark.asyncio()
async def test_list_collection_update_existing(sample_collection: Collection[SampleItem]) -> None:
updated = SampleItem(partition="alpha", index=1, name="updated", status="new")
await sample_collection.update([updated])
result = await sample_collection.get({"partition": {"exact": "alpha"}, "index": {"exact": 1}})
assert result == updated
@pytest.mark.asyncio()
async def test_list_collection_update_missing_raises(sample_collection: Collection[SampleItem]) -> None:
missing = SampleItem(partition="omega", index=99, name="missing", status="lost")
with pytest.raises(ValueError):
await sample_collection.update([missing])
@pytest.mark.asyncio()
async def test_list_collection_delete_existing(sample_collection: Collection[SampleItem]) -> None:
target = SampleItem(partition="alpha", index=1, name="ignored", status="new")
await sample_collection.delete([target])
assert (await sample_collection.size()) == 7
result = await sample_collection.get({"partition": {"exact": "alpha"}, "index": {"exact": 1}})
assert result is None
@pytest.mark.asyncio()
async def test_list_collection_delete_missing_raises(sample_collection: Collection[SampleItem]) -> None:
missing = SampleItem(partition="omega", index=3, name="x", status="y")
with pytest.raises(ValueError):
await sample_collection.delete([missing])
@pytest.mark.asyncio()
async def test_list_collection_upsert_inserts_when_missing(sample_collection: Collection[SampleItem]) -> None:
created = SampleItem(partition="omega", index=4, name="new", status="queued")
await sample_collection.upsert([created])
assert (await sample_collection.size()) == 9
fetched = await sample_collection.get({"partition": {"exact": "omega"}, "index": {"exact": 4}})
assert fetched == created
@pytest.mark.asyncio()
async def test_list_collection_upsert_updates_when_existing(sample_collection: Collection[SampleItem]) -> None:
replacement = SampleItem(partition="beta", index=2, name="replacement", status="done")
await sample_collection.upsert([replacement])
assert (await sample_collection.size()) == 8
fetched = await sample_collection.get({"partition": {"exact": "beta"}, "index": {"exact": 2}})
assert fetched == replacement
@pytest.mark.asyncio()
async def test_list_collection_upsert_get_or_insert_semantics(sample_collection: Collection[SampleItem]) -> None:
filters: Mapping[str, Any] = {"partition": {"exact": "beta"}, "index": {"exact": 2}}
original = await sample_collection.get(filters)
assert original is not None
replacement = SampleItem(
partition="beta",
index=2,
name="replacement",
status="queued",
tags=["patched"],
score=999,
rank=999,
updated_time=99.0,
payload={"priority": 99},
metadata="replacement",
)
await sample_collection.upsert([replacement], update_fields=[])
fetched = await sample_collection.get(filters)
assert fetched == original and fetched is not None
assert fetched.name == original.name
assert fetched.status == original.status
@pytest.mark.asyncio()
async def test_list_collection_upsert_updates_selected_fields(sample_collection: Collection[SampleItem]) -> None:
filters: Mapping[str, Any] = {"partition": {"exact": "beta"}, "index": {"exact": 1}}
original = await sample_collection.get(filters)
assert original is not None
incoming = SampleItem(
partition="beta",
index=1,
name="beta-incoming",
status="in-progress",
tags=["different"],
score=-1.0,
rank=42,
updated_time=123.45,
payload={"priority": -1},
metadata="incoming",
)
await sample_collection.upsert([incoming], update_fields=["status", "updated_time"])
fetched = await sample_collection.get(filters)
assert fetched is not None
assert fetched.status == incoming.status
assert fetched.updated_time == incoming.updated_time
# Ensure unspecified fields (e.g. name/tags) remain the same as the original document.
assert fetched.name == original.name
assert fetched.tags == original.tags
@pytest.mark.asyncio()
async def test_list_collection_update_returns_mutated_items(sample_collection: Collection[SampleItem]) -> None:
replacements = [
SampleItem(partition="alpha", index=1, name="alpha-new", status="patched"),
SampleItem(partition="delta", index=1, name="delta-new", status="patched"),
]
returned = await sample_collection.update(replacements)
assert list(returned) == replacements
for expected in replacements:
fetched = await sample_collection.get(
{"partition": {"exact": expected.partition}, "index": {"exact": expected.index}}
)
assert fetched == expected
original_beta = await sample_collection.get({"partition": {"exact": "beta"}, "index": {"exact": 1}})
assert original_beta is not None
partial_payload = SampleItem(
partition="beta", index=1, name="ignored", status="partial", metadata="updated-metadata"
)
partial_returned = await sample_collection.update([partial_payload], update_fields=["status", "metadata"])
assert len(partial_returned) == 1
fetched_partial = await sample_collection.get({"partition": {"exact": "beta"}, "index": {"exact": 1}})
assert fetched_partial == partial_returned[0]
assert fetched_partial is not None
assert fetched_partial.status == partial_payload.status
assert fetched_partial.metadata == partial_payload.metadata
assert fetched_partial.name == original_beta.name
@pytest.mark.asyncio()
async def test_list_collection_upsert_returns_mutated_items(sample_collection: Collection[SampleItem]) -> None:
new_item = SampleItem(partition="omega", index=99, name="omega-new", status="queued")
inserted = await sample_collection.upsert([new_item])
assert list(inserted) == [new_item]
fetched_new = await sample_collection.get({"partition": {"exact": "omega"}, "index": {"exact": 99}})
assert fetched_new == new_item
original_existing = await sample_collection.get({"partition": {"exact": "beta"}, "index": {"exact": 2}})
assert original_existing is not None
incoming = SampleItem(
partition="beta",
index=2,
name="beta-incoming-new-name",
status="processing",
tags=["beta", "patched"],
)
updated = await sample_collection.upsert([incoming], update_fields=["status", "tags"])
assert len(updated) == 1
fetched_existing = await sample_collection.get({"partition": {"exact": "beta"}, "index": {"exact": 2}})
assert fetched_existing == updated[0]
assert fetched_existing is not None
assert fetched_existing.status == incoming.status
assert fetched_existing.tags == incoming.tags
assert fetched_existing.name == original_existing.name
@pytest.mark.asyncio()
async def test_list_collection_delete_multiple_items(sample_collection: Collection[SampleItem]) -> None:
await sample_collection.delete(
[
SampleItem(partition="alpha", index=1, name="", status=""),
SampleItem(partition="beta", index=1, name="", status=""),
]
)
assert (await sample_collection.size()) == 6
@pytest.mark.asyncio()
async def test_list_collection_insert_accepts_tuple_sequence(
sample_collection: Collection[SampleItem],
) -> None:
extra = (
SampleItem(partition="tuple", index=1, name="a", status="pending"),
SampleItem(partition="tuple", index=2, name="b", status="pending"),
)
await sample_collection.insert(extra)
assert (await sample_collection.size()) == 10
fetched = await sample_collection.query(filter={"partition": {"exact": "tuple"}})
assert _sorted_pairs(fetched.items) == [("tuple", 1), ("tuple", 2)]
@pytest.mark.asyncio()
async def test_list_collection_query_without_filters_returns_all(
sample_collection: Collection[SampleItem],
) -> None:
result = await sample_collection.query()
assert result.total == 8
assert len(result.items) == 8
@pytest.mark.asyncio()
@pytest.mark.parametrize(
("filters", "expected"),
[
pytest.param({"status": {"exact": "new"}}, [("alpha", 1), ("beta", 1)], id="exact-single-field"),
pytest.param(
{"partition": {"exact": "alpha"}, "index": {"exact": 2}},
[("alpha", 2)],
id="exact-multiple-fields",
),
pytest.param(
{"status": {"within": {"running", "blocked"}}},
[("alpha", 2), ("alpha", 3), ("gamma", 1)],
id="within-set",
),
pytest.param(
{"partition": {"within": ["gamma", "delta"]}},
[("gamma", 1), ("gamma", 2), ("delta", 1)],
id="within-list",
),
pytest.param(
{"name": {"contains": "phase"}},
[("alpha", 1), ("alpha", 2), ("alpha", 3), ("gamma", 1)],
id="contains-substring",
),
pytest.param(
{"tags": {"contains": "urgent"}},
[("alpha", 1), ("beta", 1)],
id="contains-list",
),
pytest.param({"metadata": {"contains": "phase"}}, [("alpha", 3), ("gamma", 1)], id="contains-with-none-values"),
pytest.param({"tags": {"contains": "missing"}}, [], id="contains-no-match"),
pytest.param({"partition": {"exact": "delta"}}, [("delta", 1)], id="single-exact-match"),
pytest.param({"missing": {"exact": "value"}}, [], id="exact-missing-field"),
pytest.param({"score": {"contains": "phase"}}, [], id="contains-typeerror"),
pytest.param({"name": {"contains": None}}, list(BASE_KEY_ORDER), id="contains-null-check"),
pytest.param({"status": {"exact": None}}, list(BASE_KEY_ORDER), id="exact-null-no-filter"),
pytest.param({"status": {"within": 1}}, [], id="within-non-iterable"),
],
)
async def test_list_collection_query_filters(
sample_collection: Collection[SampleItem],
filters: Dict[str, Dict[str, object]],
expected: Sequence[Tuple[str, int]],
request: pytest.FixtureRequest,
) -> None:
# Mongo implementation raises ValueError for non-iterable values in within filter
if request.node.callspec.id == "mongo-within-non-iterable": # type: ignore
with pytest.raises(ValueError):
await sample_collection.query(filter=filters) # type: ignore[arg-type]
return
result = await sample_collection.query(filter=filters) # type: ignore[arg-type]
assert _sorted_pairs(result.items) == sorted(expected)
assert result.total == len(expected)
@pytest.mark.asyncio()
@pytest.mark.parametrize(
("filters", "filter_logic", "expected"),
[
(
{"status": {"exact": "new"}, "tags": {"contains": "beta"}},
"and",
[("beta", 1)],
),
(
{"status": {"exact": "new"}, "tags": {"contains": "beta"}},
"or",
[("alpha", 1), ("beta", 1), ("beta", 2)],
),
(
{"status": {"exact": "done"}, "tags": {"contains": "core"}},
"and",
[("gamma", 2)],
),
(
{"status": {"exact": "done"}, "tags": {"contains": "core"}},
"or",
[("alpha", 1), ("alpha", 2), ("beta", 2), ("gamma", 2)],
),
],
)
async def test_list_collection_filter_logic(
sample_collection: Collection[SampleItem],
filters: Dict[str, Dict[str, object]],
filter_logic: Literal["and", "or"],
expected: Sequence[Tuple[str, int]],
) -> None:
filter_payload = dict(filters)
filter_payload["_aggregate"] = filter_logic # type: ignore[index]
result = await sample_collection.query(filter=filter_payload) # type: ignore[arg-type]
assert _sorted_pairs(result.items) == sorted(expected)
@pytest.mark.asyncio()
async def test_list_collection_must_filters_respected_with_or(
sample_collection: Collection[SampleItem],
) -> None:
filters = {
"_aggregate": "or",
"_must": {"partition": {"exact": "alpha"}},
"status": {"exact": "done"},
"tags": {"contains": "urgent"},
}
result = await sample_collection.query(filter=filters) # type: ignore[arg-type]
assert _sorted_pairs(result.items) == [("alpha", 1)]
@pytest.mark.asyncio()
async def test_list_collection_must_filters_accept_sequence(
sample_collection: Collection[SampleItem],
) -> None:
filters = {
"_aggregate": "or",
"_must": [
{"partition": {"exact": "beta"}},
{"index": {"exact": 2}},
],
"status": {"exact": "new"},
"tags": {"contains": "beta"},
}
result = await sample_collection.query(filter=filters) # type: ignore[arg-type]
assert _sorted_pairs(result.items) == [("beta", 2)]
@pytest.mark.asyncio()
async def test_list_collection_must_filters_limit_tree_scan_even_with_or(
sample_collection: Collection[SampleItem],
monkeypatch: pytest.MonkeyPatch,
) -> None:
if not isinstance(sample_collection, ListBasedCollection):
pytest.skip("This test is only valid for pure-memory collections")
seen: List[Tuple[str, int]] = []
original = _item_matches_filters
def tracking(
item: SampleItem,
filters: object,
filter_logic: str,
must_filters: object | None = None,
) -> bool:
seen.append((item.partition, item.index))
return original(item, filters, filter_logic, must_filters) # type: ignore[arg-type]
monkeypatch.setattr(memory_module, "_item_matches_filters", tracking)
filters = {
"_aggregate": "or",
"_must": {"partition": {"exact": "gamma"}},
"status": {"exact": "done"},
"tags": {"contains": "urgent"},
}
result = await sample_collection.query(filter=filters) # type: ignore[arg-type]
assert _sorted_pairs(result.items) == [("gamma", 2)]
assert set(seen) == {("gamma", 1), ("gamma", 2)}
@pytest.mark.asyncio()
async def test_list_collection_primary_key_prefix_limits_filter_checks(
sample_items: Sequence[SampleItem],
monkeypatch: pytest.MonkeyPatch,
) -> None:
collection = _build_collection(sample_items)
seen: List[Tuple[str, int]] = []
original = _item_matches_filters
def tracking(
item: SampleItem,
filters: object,
filter_logic: str,
must_filters: object | None = None,
) -> bool:
seen.append((item.partition, item.index))
return original(item, filters, filter_logic, must_filters) # type: ignore[arg-type]
monkeypatch.setattr(memory_module, "_item_matches_filters", tracking)
filters = {"partition": {"exact": "alpha"}, "index": {"within": {1, 2}}}
result = await collection.query(filter=filters) # type: ignore[arg-type]
assert _sorted_pairs(result.items) == [("alpha", 1), ("alpha", 2)]
assert set(seen) == {("alpha", 1), ("alpha", 2), ("alpha", 3)}
@pytest.mark.asyncio()
async def test_list_collection_full_primary_key_avoids_tree_scan(
sample_collection: Collection[SampleItem],
monkeypatch: pytest.MonkeyPatch,
) -> None:
if not isinstance(sample_collection, ListBasedCollection):
pytest.skip("This test is only valid for pure-memory collections")
call_count = 0
original_iter_items = ( # pyright: ignore[reportPrivateUsage,reportUnknownMemberType,reportUnknownVariableType]
ListBasedCollection._iter_items # pyright: ignore[reportPrivateUsage,reportUnknownMemberType]
)
def tracking(
self: ListBasedCollection[SampleItem],
root: Mapping[str, object] | None = None,
filters: object | None = None,
must_filters: object | None = None,
filter_logic: str = "and",
) -> Iterable[SampleItem]:
nonlocal call_count
call_count += 1
return original_iter_items(self, root, filters, must_filters, filter_logic) # type: ignore[arg-type]
monkeypatch.setattr(ListBasedCollection, "_iter_items", tracking)
filters = {"partition": {"exact": "beta"}, "index": {"exact": 2}}
result = await sample_collection.query(filter=filters) # type: ignore[arg-type]
assert _sorted_pairs(result.items) == [("beta", 2)]
assert call_count == 0
@pytest.mark.asyncio()
@pytest.mark.parametrize(
("sort_by", "sort_order", "limit", "expected"),
[
("name", "asc", 4, [("beta", 2), ("beta", 1), ("alpha", 3), ("delta", 1)]),
("name", "desc", 4, [("alpha", 1), ("alpha", 2), ("gamma", 1), ("gamma", 2)]),
("rank", "asc", 4, [("beta", 2), ("beta", 1), ("alpha", 2), ("alpha", 1)]),
("rank", "desc", 4, [("delta", 1), ("gamma", 2), ("alpha", 3), ("gamma", 1)]),
("score", "asc", 4, [("alpha", 3), ("gamma", 2), ("beta", 2), ("delta", 1)]),
("score", "desc", 4, [("alpha", 1), ("gamma", 1), ("beta", 1), ("alpha", 2)]),
(
"updated_time",
"asc",
4,
(
[("alpha", 2), ("gamma", 1), ("beta", 2), ("beta", 1)],
[("beta", 2), ("beta", 1), ("delta", 1), ("alpha", 1)],
),
),
(
"updated_time",
"desc",
4,
(
[("gamma", 2), ("alpha", 3), ("alpha", 1), ("delta", 1)],
[("gamma", 1), ("alpha", 2), ("gamma", 2), ("alpha", 3)],
),
),
],
)
async def test_list_collection_sorting(
sample_collection: Collection[SampleItem],
sort_by: str,
sort_order: str,
limit: int,
expected: Union[Sequence[Tuple[str, int]], Tuple[Sequence[Tuple[str, int]], ...]],
) -> None:
result = await sample_collection.query(sort={"name": sort_by, "order": sort_order}, limit=limit) # type: ignore[arg-type]
if isinstance(expected, tuple):
assert any(_key_pairs(result.items) == list(expected) for expected in expected)
else:
assert _key_pairs(result.items) == list(expected)
@pytest.mark.asyncio()
async def test_list_collection_sort_by_missing_field_raises(sample_collection: Collection[SampleItem]) -> None:
with pytest.raises(ValueError):
await sample_collection.query(sort={"name": "does_not_exist", "order": "asc"})
@pytest.mark.asyncio()
@pytest.mark.parametrize(
("limit", "offset", "expected", "total"),
[
(1, 0, [("alpha", 1)], 3),
(2, 1, [("alpha", 2), ("alpha", 3)], 3),
(-1, 1, [("alpha", 2), ("alpha", 3)], 3),
(10, 0, [("alpha", 1), ("alpha", 2), ("alpha", 3)], 3),
(0, 0, [], 3),
(1, 10, [], 3),
],
)
async def test_list_collection_pagination_without_sort(
sample_collection: Collection[SampleItem],
limit: int,
offset: int,
expected: Sequence[Tuple[str, int]],
total: int,
) -> None:
result = await sample_collection.query(filter={"partition": {"exact": "alpha"}}, limit=limit, offset=offset)
assert _key_pairs(result.items) == list(expected)
assert result.total == total
@pytest.mark.asyncio()
async def test_list_collection_pagination_with_sort(sample_collection: Collection[SampleItem]) -> None:
result = await sample_collection.query(sort={"name": "name", "order": "asc"}, limit=2, offset=3)
assert _key_pairs(result.items) == [("delta", 1), ("gamma", 2)]
assert result.total == 8
@pytest.mark.asyncio()
async def test_list_collection_limit_unbounded_with_sort(sample_collection: Collection[SampleItem]) -> None:
result = await sample_collection.query(sort={"name": "name", "order": "asc"}, limit=-1, offset=6)
assert _key_pairs(result.items) == [("alpha", 2), ("alpha", 1)]
assert result.total == 8
@pytest.mark.asyncio()
async def test_list_collection_limit_zero_reports_total(sample_collection: Collection[SampleItem]) -> None:
result = await sample_collection.query(filter={"status": {"exact": "done"}}, limit=0)
assert result.items == []
assert result.total == 2
@pytest.mark.asyncio()
async def test_list_collection_offset_beyond_total_returns_empty(
sample_collection: Collection[SampleItem],
) -> None:
result = await sample_collection.query(filter={"status": {"exact": "done"}}, offset=10)
assert result.items == []
assert result.total == 2
@pytest.mark.asyncio()
async def test_list_collection_query_reports_total_with_limit(
sample_collection: Collection[SampleItem],
) -> None:
result = await sample_collection.query(filter={"partition": {"exact": "alpha"}}, limit=1)
assert result.total == 3
assert len(result.items) == 1
@pytest.mark.asyncio()
async def test_list_collection_get_returns_first_match(sample_collection: Collection[SampleItem]) -> None:
item = await sample_collection.get({"status": {"exact": "new"}})
assert item is not None
assert (item.partition, item.index) in [("beta", 1), ("alpha", 1)]
@pytest.mark.asyncio()
async def test_list_collection_get_returns_none(sample_collection: Collection[SampleItem]) -> None:
result = await sample_collection.get({"partition": {"exact": "missing"}})
assert result is None
@pytest.mark.asyncio()
async def test_list_collection_get_respects_filter_logic(sample_collection: Collection[SampleItem]) -> None:
filters = {"status": {"exact": "done"}, "tags": {"contains": "urgent"}, "_aggregate": "or"}
item = await sample_collection.get(filters) # type: ignore[arg-type]
assert item is not None
assert (item.partition, item.index) in [("gamma", 2), ("alpha", 1)]
@pytest.mark.asyncio()
async def test_list_collection_get_honors_sort_by(sample_collection: Collection[SampleItem]) -> None:
filters = {"partition": {"exact": "alpha"}}
item = await sample_collection.get(filters, sort={"name": "rank", "order": "asc"}) # type: ignore[arg-type]
assert item is not None
assert (item.partition, item.index) == ("alpha", 2)
@pytest.mark.asyncio()
async def test_list_collection_get_honors_sort_order(sample_collection: Collection[SampleItem]) -> None:
filters = {"partition": {"exact": "alpha"}}
item = await sample_collection.get(filters, sort={"name": "rank", "order": "desc"}) # type: ignore[arg-type]
assert item is not None
assert (item.partition, item.index) == ("alpha", 3)
@pytest.mark.asyncio()
async def test_list_collection_query_handles_large_dataset() -> None:
bulk_items = [
SampleItem(
partition=f"partition-{i % 5}",
index=i,
name=f"bulk-{i}",
status="bulk",
score=float(i),
rank=i,
updated_time=float(i),
)
for i in range(1500)
]
collection = _build_collection(bulk_items)
result = await collection.query(sort={"name": "index", "order": "asc"}, limit=50, offset=100)
assert result.total == 1500
assert len(result.items) == 50
assert result.items[0].index == 100
assert result.items[-1].index == 149
@pytest.mark.asyncio()
async def test_list_collection_bulk_delete_and_size() -> None:
items = [SampleItem(partition="bulk", index=i, name=f"item-{i}", status="bulk") for i in range(40)]
collection = _build_collection(items)
await collection.delete(items[:20])
assert (await collection.size()) == 20
await collection.delete(items[20:])
assert (await collection.size()) == 0
@pytest.mark.asyncio()
async def test_list_collection_query_rejects_unknown_operator(
sample_collection: Collection[SampleItem],
) -> None:
with pytest.raises(ValueError):
await sample_collection.query(filter={"status": {"invalid": "x"}}) # type: ignore[arg-type]
@pytest.mark.asyncio()
async def test_list_collection_query_result_type() -> None:
collection = _build_collection([])
result = await collection.query(filter=None)
assert result.items == []
assert result.offset == 0
@pytest.mark.asyncio()
async def test_deque_queue_initial_size(deque_queue: DequeBasedQueue[QueueItem]) -> None:
assert (await deque_queue.size()) == 3
def test_deque_queue_item_type(deque_queue: DequeBasedQueue[QueueItem]) -> None:
assert deque_queue.item_type() is QueueItem
@pytest.mark.asyncio()
async def test_deque_queue_has_detects_members(deque_queue: DequeBasedQueue[QueueItem]) -> None:
assert await deque_queue.has(QueueItem(idx=1))
assert not await deque_queue.has(QueueItem(idx=99))
@pytest.mark.asyncio()
async def test_deque_queue_enqueue_appends_items(deque_queue: DequeBasedQueue[QueueItem]) -> None:
items = [QueueItem(idx=3), QueueItem(idx=4)]
returned = await deque_queue.enqueue(items)
assert returned == items
assert (await deque_queue.size()) == 5
@pytest.mark.asyncio()
async def test_deque_queue_enqueue_rejects_wrong_type(deque_queue: DequeBasedQueue[QueueItem]) -> None:
class Wrong(BaseModel):
idx: int
with pytest.raises(TypeError):
await deque_queue.enqueue([Wrong(idx=9)]) # type: ignore[arg-type]
@pytest.mark.asyncio()
@pytest.mark.parametrize("limit", [1, 2, 5])
async def test_deque_queue_dequeue_respects_limit(deque_queue: DequeBasedQueue[QueueItem], limit: int) -> None:
result = await deque_queue.dequeue(limit)
assert len(result) == min(limit, 3)
assert (await deque_queue.size()) == 3 - min(limit, 3)
@pytest.mark.asyncio()
async def test_deque_queue_dequeue_zero_returns_empty(deque_queue: DequeBasedQueue[QueueItem]) -> None:
assert await deque_queue.dequeue(0) == []
@pytest.mark.asyncio()
async def test_deque_queue_dequeue_more_than_available(deque_queue: DequeBasedQueue[QueueItem]) -> None:
result = await deque_queue.dequeue(10)
assert len(result) == 3
assert (await deque_queue.size()) == 0
@pytest.mark.asyncio()
async def test_deque_queue_peek_preserves_items(deque_queue: DequeBasedQueue[QueueItem]) -> None:
snapshot = await deque_queue.peek(2)
assert [item.idx for item in snapshot] == [0, 1]
assert (await deque_queue.size()) == 3
@pytest.mark.asyncio()
async def test_deque_queue_peek_zero_returns_empty(deque_queue: DequeBasedQueue[QueueItem]) -> None:
assert await deque_queue.peek(0) == []
@pytest.mark.asyncio()
async def test_deque_queue_peek_after_partial_dequeue(deque_queue: DequeBasedQueue[QueueItem]) -> None:
await deque_queue.dequeue(1)
snapshot = await deque_queue.peek(2)
assert [item.idx for item in snapshot] == [1, 2]
@pytest.mark.asyncio()
async def test_deque_queue_handles_large_volume() -> None:
queue = DequeBasedQueue(QueueItem)
items = [QueueItem(idx=i) for i in range(2000)]
await queue.enqueue(items)
assert (await queue.size()) == 2000
drained = await queue.dequeue(1500)
assert len(drained) == 1500
assert (await queue.size()) == 500
@pytest.mark.asyncio()
async def test_dict_key_value_initial_state(dict_key_value: DictBasedKeyValue[str, int]) -> None:
assert await dict_key_value.size() == 2
assert await dict_key_value.get("alpha") == 1
assert await dict_key_value.get("missing") is None
@pytest.mark.asyncio()
async def test_dict_key_value_has_handles_presence(dict_key_value: DictBasedKeyValue[str, int]) -> None:
assert await dict_key_value.has("alpha")
assert not await dict_key_value.has("gamma")
@pytest.mark.asyncio()
async def test_dict_key_value_set_updates_and_expands(dict_key_value: DictBasedKeyValue[str, int]) -> None:
await dict_key_value.set("gamma", 3)
assert await dict_key_value.size() == 3
await dict_key_value.set("alpha", 99)
assert await dict_key_value.get("alpha") == 99
assert await dict_key_value.size() == 3
@pytest.mark.asyncio()
async def test_dict_key_value_pop_returns_default(dict_key_value: DictBasedKeyValue[str, int]) -> None:
result = await dict_key_value.pop("beta")
assert result == 2
assert await dict_key_value.size() == 1
result = await dict_key_value.pop("missing", 42)
assert result == 42
assert await dict_key_value.size() == 1
@pytest.mark.asyncio()
async def test_key_value_inc_updates_existing(dict_key_value: KeyValue[str, int]) -> None:
new_value = await dict_key_value.inc("alpha", 2)
assert new_value == 3
assert await dict_key_value.get("alpha") == 3
@pytest.mark.asyncio()
async def test_key_value_inc_initializes_missing(dict_key_value: KeyValue[str, int]) -> None:
new_value = await dict_key_value.inc("gamma", 4)
assert new_value == 4
assert await dict_key_value.has("gamma")
@pytest.mark.asyncio()
async def test_key_value_inc_rejects_non_numeric_amount(dict_key_value: KeyValue[str, int]) -> None:
with pytest.raises(TypeError):
await dict_key_value.inc("alpha", cast(Any, "invalid"))
@pytest.mark.asyncio()
async def test_key_value_chmax_updates_existing(dict_key_value: KeyValue[str, int]) -> None:
new_value = await dict_key_value.chmax("alpha", 10)
assert new_value == 10
assert await dict_key_value.get("alpha") == 10
@pytest.mark.asyncio()
async def test_key_value_chmax_ignores_smaller(dict_key_value: KeyValue[str, int]) -> None:
initial = await dict_key_value.get("alpha")
result = await dict_key_value.chmax("alpha", 0)
assert result == initial
assert await dict_key_value.get("alpha") == initial
@pytest.mark.asyncio()
async def test_key_value_chmax_initializes_missing(dict_key_value: KeyValue[str, int]) -> None:
result = await dict_key_value.chmax("gamma", 7)
assert result == 7
assert await dict_key_value.get("gamma") == 7
@pytest.mark.asyncio()
async def test_key_value_chmax_rejects_non_numeric_value(dict_key_value: KeyValue[str, int]) -> None:
with pytest.raises(TypeError):
await dict_key_value.chmax("alpha", cast(Any, "wrong"))
@pytest.mark.asyncio()
async def test_dict_key_value_inc_rejects_non_numeric_value(dict_key_value_memory: DictBasedKeyValue[str, Any]) -> None:
await dict_key_value_memory.set("alpha", cast(Any, "na"))
with pytest.raises(TypeError):
await dict_key_value_memory.inc("alpha", 1)
@pytest.mark.asyncio()
async def test_dict_key_value_chmax_rejects_non_numeric_value(
dict_key_value_memory: DictBasedKeyValue[str, Any],
) -> None:
await dict_key_value_memory.set("alpha", cast(Any, "na"))
with pytest.raises(TypeError):
await dict_key_value_memory.chmax("alpha", 1)
@pytest.mark.mongo
@pytest.mark.asyncio()
async def test_mongo_key_value_inc_rejects_non_numeric_value(temporary_mongo_database: AsyncDatabase[Any]) -> None:
from agentlightning.store.collection.mongo import MongoBasedKeyValue, MongoClientPool
async with MongoClientPool[Mapping[str, Any]](
mongo_uri=mongo_uri, mongo_client_kwargs=mongo_client_kwargs
) as client_pool:
key_value = MongoBasedKeyValue[str, int](
client_pool,
temporary_mongo_database.name,
f"kv-inc-{uuid4().hex}",
"partition-inc",
str,
int,
)
collection = await key_value.ensure_collection()
await collection.insert_one(
{
"partition_id": "partition-inc",
"key": "alpha",
"value": "oops",
}
)
with pytest.raises(TypeError):
await key_value.inc("alpha", 1)
@pytest.mark.mongo
@pytest.mark.asyncio()
async def test_mongo_key_value_chmax_behaves_like_max(temporary_mongo_database: AsyncDatabase[Any]) -> None:
from agentlightning.store.collection.mongo import MongoBasedKeyValue, MongoClientPool
async with MongoClientPool[Mapping[str, Any]](
mongo_uri=mongo_uri, mongo_client_kwargs=mongo_client_kwargs
) as client_pool:
key_value = MongoBasedKeyValue[str, int](
client_pool,
temporary_mongo_database.name,
f"kv-chmax-{uuid4().hex}",
"partition-chmax",
str,
int,
)
assert await key_value.chmax("alpha", 5) == 5
assert await key_value.chmax("alpha", 3) == 5
assert await key_value.chmax("alpha", 9) == 9
assert await key_value.get("alpha") == 9
@pytest.mark.mongo
@pytest.mark.asyncio()
async def test_mongo_key_value_chmax_rejects_non_numeric_value(temporary_mongo_database: AsyncDatabase[Any]) -> None:
from agentlightning.store.collection.mongo import MongoBasedKeyValue, MongoClientPool
async with MongoClientPool[Mapping[str, Any]](
mongo_uri=mongo_uri, mongo_client_kwargs=mongo_client_kwargs
) as client_pool:
key_value = MongoBasedKeyValue[str, int](
client_pool,
temporary_mongo_database.name,
f"kv-chmax-bad-{uuid4().hex}",
"partition-chmax-bad",
str,
int,
)
collection = await key_value.ensure_collection()
await collection.insert_one(
{
"partition_id": "partition-chmax-bad",
"key": "alpha",
"value": "oops",
}
)
with pytest.raises((TypeError, pydantic.ValidationError)):
await key_value.chmax("alpha", 1)
def test_thread_safe_async_lock_blocks_threads() -> None:
lock = _ThreadSafeAsyncLock()
allow_second = threading.Event()
second_has_lock = threading.Event()
release_first = threading.Event()
async def first() -> None:
async with lock:
allow_second.set()
release_first.wait()
async def second() -> None:
allow_second.wait()
async with lock:
second_has_lock.set()
def thread1() -> None:
asyncio.run(first())
def thread2() -> None:
asyncio.run(second())
t1 = threading.Thread(target=thread1)
t2 = threading.Thread(target=thread2)
t1.start()
t2.start()
assert allow_second.wait(timeout=1)
assert not second_has_lock.wait(0.05)
release_first.set()
t1.join(timeout=1)
t2.join(timeout=1)
assert second_has_lock.is_set()
@pytest.mark.asyncio()
async def test_thread_safe_async_lock_serializes_async_tasks() -> None:
lock = _ThreadSafeAsyncLock()
first_entered = asyncio.Event()
release_first = asyncio.Event()
second_acquired = asyncio.Event()
async def first() -> None:
async with lock:
first_entered.set()
await release_first.wait()
async def second() -> None:
await first_entered.wait()
async with lock:
second_acquired.set()
task1 = asyncio.create_task(first())
task2 = asyncio.create_task(second())
await first_entered.wait()
await asyncio.sleep(0)
assert not second_acquired.is_set()
release_first.set()
await asyncio.gather(task1, task2)
assert second_acquired.is_set()
@pytest.mark.asyncio()
async def test_loop_aware_async_lock_serializes_tasks() -> None:
lock = _LoopAwareAsyncLock()
first_entered = asyncio.Event()
release_first = asyncio.Event()
second_acquired = asyncio.Event()
async def first() -> None:
async with lock:
first_entered.set()
await release_first.wait()
async def second() -> None:
await first_entered.wait()
async with lock:
second_acquired.set()
task1 = asyncio.create_task(first())
task2 = asyncio.create_task(second())
await first_entered.wait()
await asyncio.sleep(0)
assert not second_acquired.is_set()
release_first.set()
await asyncio.gather(task1, task2)
assert second_acquired.is_set()
@pytest.mark.asyncio()
async def test_loop_aware_async_lock_reuses_loop_specific_lock() -> None:
lock = _LoopAwareAsyncLock()
first_lock: asyncio.Lock | None = None
async with lock as acquired:
first_lock = acquired
assert first_lock.locked()
assert first_lock is not None and not first_lock.locked()
async with lock as acquired_again:
assert acquired_again is first_lock
@pytest.mark.asyncio()
async def test_loop_aware_async_lock_distinguishes_event_loops() -> None:
lock = _LoopAwareAsyncLock()
main_loop_lock: asyncio.Lock | None = None
async with lock as acquired:
main_loop_lock = acquired
locks_from_threads: List[asyncio.Lock] = []
def _worker() -> None:
async def runner() -> None:
async with lock as acquired:
locks_from_threads.append(acquired)
asyncio.run(runner())
worker = threading.Thread(target=_worker)
worker.start()
worker.join(timeout=2)
assert worker.is_alive() is False
assert main_loop_lock is not None
assert locks_from_threads
assert locks_from_threads[0] is not main_loop_lock
@pytest.mark.asyncio()
async def test_dict_key_value_does_not_mutate_input_mapping(dict_key_value_data: Dict[str, int]) -> None:
key_value = DictBasedKeyValue(dict_key_value_data)
await key_value.set("gamma", 3) # type: ignore[arg-type]
await key_value.pop("alpha") # type: ignore[arg-type]
assert dict_key_value_data == {"alpha": 1, "beta": 2}
@pytest.mark.asyncio()
async def test_inmemory_atomic_read_only_skips_lock() -> None:
collections = memory_module.InMemoryLightningCollections(lock_type="asyncio")
class FailingLock:
async def __aenter__(self) -> None:
raise AssertionError("read-only atomic block should not acquire the lock")
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
return None
collections._lock = {"default": FailingLock()} # type: ignore[attr-defined]
async with collections.atomic(mode="r", snapshot=False):
# Should complete without touching the failing lock.
assert collections.rollouts is not None
@pytest.mark.asyncio()
async def test_inmemory_atomic_snapshot_or_write_acquires_lock() -> None:
collections = memory_module.InMemoryLightningCollections(lock_type="asyncio")
class RecordingLock:
def __init__(self) -> None:
self.enter_count = 0
self.exit_count = 0
async def __aenter__(self) -> None:
self.enter_count += 1
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
self.exit_count += 1
lock = RecordingLock()
collections._lock = {"default": lock} # type: ignore[attr-defined]
async with collections.atomic(mode="rw", snapshot=False):
assert collections.attempts is not None
assert (lock.enter_count, lock.exit_count) == (1, 1)
lock.enter_count = lock.exit_count = 0
async with collections.atomic(mode="r", snapshot=True):
assert collections.spans is not None
assert (lock.enter_count, lock.exit_count) == (1, 1)
@pytest.mark.mongo
@pytest.mark.asyncio()
async def test_mongo_based_sanity_check(temporary_mongo_database: AsyncDatabase[Any]) -> None:
from agentlightning.store.collection.mongo import (
MongoBasedCollection,
MongoBasedKeyValue,
MongoBasedQueue,
MongoClientPool,
)
async with MongoClientPool[Mapping[str, Any]](
mongo_uri=mongo_uri, mongo_client_kwargs=mongo_client_kwargs
) as client_pool:
collection = MongoBasedCollection[Any](
client_pool, temporary_mongo_database.name, "test", "test-123", ["rollout_id"], Rollout
)
await collection.ensure_collection()
start_time = time.time()
await collection.insert(
[Rollout(rollout_id="test-123", input="test-123", start_time=start_time, status="running")]
)
result = await collection.query(filter={"status": {"exact": "running"}})
assert result.items == [
Rollout(rollout_id="test-123", input="test-123", start_time=start_time, status="running")
]
rollout_queue = MongoBasedQueue[str](
client_pool, temporary_mongo_database.name, "rollout_queue", "partition-1", str
)
await rollout_queue.ensure_collection()
await rollout_queue.enqueue(["r1", "r2", "r3"])
assert await rollout_queue.size() == 3
assert await rollout_queue.peek(2) == ["r1", "r2"]
assert await rollout_queue.dequeue(2) == ["r1", "r2"]
assert await rollout_queue.size() == 1
span_kv = MongoBasedKeyValue[str, int](
client_pool, temporary_mongo_database.name, "span_sequence_ids", "partition-1", str, int
)
await span_kv.ensure_collection()
await span_kv.set("span-123", 1)
assert await span_kv.has("span-123")
assert await span_kv.get("span-123") == 1
assert await span_kv.pop("span-123") == 1
assert not await span_kv.has("span-123")
@pytest.mark.mongo
@pytest.mark.asyncio()
async def test_mongo_ensure_collection_creates_partition_scoped_index(
temporary_mongo_database: AsyncDatabase[Any],
) -> None:
from agentlightning.store.collection.mongo import MongoBasedCollection, MongoClientPool
collection_name = f"ensure-{uuid4().hex}"
async with MongoClientPool[Mapping[str, Any]](
mongo_uri=mongo_uri, mongo_client_kwargs=mongo_client_kwargs
) as client_pool:
collection = MongoBasedCollection[Any](
client_pool,
temporary_mongo_database.name,
collection_name,
"partition-ensure",
["name", "index"],
SampleItem,
)
await collection.ensure_collection()
unique_index = None
async for index in await temporary_mongo_database[collection_name].list_indexes(): # type: ignore
if index["name"] == "uniq_partition_name_index" and index.get("unique"): # type: ignore
unique_index = index # type: ignore
break
assert unique_index is not None, "expected unique partition/index key"
key_pairs = list(unique_index["key"].items()) # type: ignore
assert key_pairs == [("partition_id", 1), ("name", 1), ("index", 1)]
@pytest.mark.mongo
@pytest.mark.asyncio()
async def test_mongo_ensure_collection_survives_concurrent_calls(temporary_mongo_database: AsyncDatabase[Any]) -> None:
from agentlightning.store.collection.mongo import MongoBasedCollection, MongoClientPool
collection_name = f"ensure-{uuid4().hex}"
async def ensure_once() -> None:
async with MongoClientPool[Mapping[str, Any]](
mongo_uri=mongo_uri, mongo_client_kwargs=mongo_client_kwargs
) as client_pool:
collection = MongoBasedCollection(
client_pool,
temporary_mongo_database.name,
collection_name,
"partition-concurrent",
["index"],
SampleItem,
)
await collection.ensure_collection()
await asyncio.gather(*(ensure_once() for _ in range(20)))
names = await temporary_mongo_database.list_collection_names()
assert names.count(collection_name) == 1
unique_indexes = []
async for index in await temporary_mongo_database[collection_name].list_indexes(): # type: ignore
if index["name"].startswith("uniq_partition_"): # type: ignore
unique_indexes.append(index["name"]) # type: ignore
assert unique_indexes == ["uniq_partition_index"]
@pytest.mark.mongo
@pytest.mark.asyncio()
async def test_mongo_ensure_collection_repeats_without_altering_indexes(
temporary_mongo_database: AsyncDatabase[Any],
) -> None:
from agentlightning.store.collection.mongo import MongoBasedCollection, MongoClientPool
collection_name = f"ensure-{uuid4().hex}"
async with MongoClientPool[Mapping[str, Any]](
mongo_uri=mongo_uri, mongo_client_kwargs=mongo_client_kwargs
) as client_pool:
collection = MongoBasedCollection(
client_pool, temporary_mongo_database.name, collection_name, "partition-repeat", ["index"], SampleItem
)
await collection.ensure_collection()
await collection.ensure_collection()
unique_indexes = []
async for index in await temporary_mongo_database[collection_name].list_indexes(): # type: ignore
if index["name"].startswith("uniq_partition_"): # type: ignore
unique_indexes.append((index["name"], list(index["key"].items()))) # type: ignore
assert unique_indexes == [("uniq_partition_index", [("partition_id", 1), ("index", 1)])]
async def _with_mongo_collections(
db: AsyncDatabase[Any],
callback: Callable[[MongoLightningCollections], Awaitable[Any]],
) -> Any:
from agentlightning.store.collection.mongo import MongoClientPool, MongoLightningCollections
async with MongoClientPool[Mapping[str, Any]](
mongo_uri=mongo_uri, mongo_client_kwargs=mongo_client_kwargs
) as client_pool:
collections = MongoLightningCollections(
client_pool=client_pool,
database_name=db.name,
partition_id=f"partition-{uuid4().hex}",
)
return await callback(collections)
async def _initialize_counter(collections: MongoLightningCollections, key: str) -> None:
async def _init(coll: MongoLightningCollections) -> None:
await coll.span_sequence_ids.set(key, 0)
await collections.execute(_init, commit=False)
async def _read_counter(collections: MongoLightningCollections, key: str) -> int:
async def _read(coll: MongoLightningCollections) -> int:
value = await coll.span_sequence_ids.get(key)
assert value is not None
return value
return await collections.execute(_read, commit=False)
async def _contention_run(
collections: MongoLightningCollections,
*,
key: str,
commit: bool,
concurrency: int,
) -> int:
read_lock = asyncio.Lock()
ready = asyncio.Event()
readers_seen = 0
async def _barrier() -> None:
nonlocal readers_seen
async with read_lock:
readers_seen += 1
if readers_seen == concurrency:
ready.set()
await ready.wait()
async def worker(_: int) -> None:
first_attempt = True
async def callback(coll: MongoLightningCollections) -> None:
nonlocal first_attempt
value = await coll.span_sequence_ids.get(key)
assert value is not None
if first_attempt:
await _barrier()
first_attempt = False
await asyncio.sleep(0)
await coll.span_sequence_ids.set(key, value + 1)
await collections.execute(callback, commit=commit, snapshot=True, mode="rw")
await asyncio.gather(*(worker(i) for i in range(concurrency)))
return await _read_counter(collections, key)
@pytest.mark.mongo
@pytest.mark.asyncio()
async def test_mongo_execute_without_commit_allows_lost_updates(
temporary_mongo_database: AsyncDatabase[Any],
) -> None:
async def scenario(collections: MongoLightningCollections) -> None:
counter_key = f"counter-{uuid4().hex}"
await _initialize_counter(collections, counter_key)
final_value = await _contention_run(
collections,
key=counter_key,
commit=False,
concurrency=6,
)
assert final_value == 1
await _with_mongo_collections(temporary_mongo_database, scenario)
@pytest.mark.mongo
@pytest.mark.asyncio()
async def test_mongo_execute_with_commit_retries_until_success(
temporary_mongo_database: AsyncDatabase[Any],
) -> None:
async def scenario(collections: MongoLightningCollections) -> None:
counter_key = f"counter-{uuid4().hex}"
await _initialize_counter(collections, counter_key)
final_value = await _contention_run(
collections,
key=counter_key,
commit=True,
concurrency=6,
)
assert final_value == 6
await _with_mongo_collections(temporary_mongo_database, scenario)