Initial curated RAGcore source from c16667f0c9397e2bfc8dd0255b114fba23c8d05e

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:34:21 +02:00
commit 7ca726caf2
659 files changed
+98171

No files matched your search

@@ -0,0 +1,78 @@
"""ConcurrencyBudget: overload is bounded, not silently queued forever."""
from __future__ import annotations
import asyncio
import pytest
from ragcore.application.limits import ConcurrencyBudget, ConcurrencyExceededError
async def test_overload_is_bounded_not_queued_without_limit() -> None:
budget = ConcurrencyBudget(name="model-calls", max_concurrent=1, max_queued=1)
release = asyncio.Event()
async def occupy() -> None:
async with budget.acquire():
await release.wait()
first = asyncio.create_task(occupy())
await asyncio.sleep(0)
second = asyncio.create_task(occupy())
await asyncio.sleep(0)
assert budget.waiting == 1
with pytest.raises(ConcurrencyExceededError):
async with budget.acquire():
pass # pragma: no cover -- must never be entered
release.set()
await first
await second
async def test_a_call_the_semaphore_can_serve_immediately_is_never_queued() -> None:
budget = ConcurrencyBudget(name="model-calls", max_concurrent=1, max_queued=0)
async with budget.acquire():
pass # the only slot is free, so this must succeed even with max_queued=0
async with budget.acquire():
pass
async def test_cancellation_while_waiting_releases_the_queue_slot() -> None:
budget = ConcurrencyBudget(name="model-calls", max_concurrent=1, max_queued=1)
release = asyncio.Event()
async def occupy() -> None:
async with budget.acquire():
await release.wait()
holder = asyncio.create_task(occupy())
await asyncio.sleep(0)
async def waiter() -> None:
async with budget.acquire():
pass # pragma: no cover -- cancelled before ever acquiring
waiting_task = asyncio.create_task(waiter())
await asyncio.sleep(0)
assert budget.waiting == 1
waiting_task.cancel()
with pytest.raises(asyncio.CancelledError):
await waiting_task
assert budget.waiting == 0
release.set()
await holder
def test_invalid_construction_arguments_are_rejected() -> None:
with pytest.raises(ValueError, match="name"):
ConcurrencyBudget(name="", max_concurrent=1, max_queued=0)
with pytest.raises(ValueError, match="max_concurrent"):
ConcurrencyBudget(name="x", max_concurrent=0, max_queued=0)
with pytest.raises(ValueError, match="max_queued"):
ConcurrencyBudget(name="x", max_concurrent=1, max_queued=-1)
@@ -0,0 +1,125 @@
"""RateLimitMiddleware: operation-scoped limits that survive an alternate endpoint."""
from __future__ import annotations
from datetime import UTC, datetime
from fastapi import FastAPI, Request
from fastapi.testclient import TestClient
from ragcore.api.middleware import RateLimitMiddleware
from ragcore.application.audit import AuditEventService
from ragcore.application.limits import RateLimitRegistry, RateLimitRule
from ragcore.application.limits.rate_limiter import RateLimiter
class _NullAuditRepository:
async def append(self, event: object) -> None: # pragma: no cover -- unused here
raise AssertionError("no admin overrides are exercised in these tests")
async def list_events(self, *args: object, **kwargs: object) -> tuple[object, ...]:
return ()
def _fixed_subject(request: Request) -> str:
return request.headers.get("x-test-subject", "anonymous")
def _app(*, rules: dict[str, RateLimitRule]) -> tuple[FastAPI, RateLimiter]:
audit = AuditEventService(_NullAuditRepository(), clock=lambda: datetime.now(UTC))
registry = RateLimitRegistry(default_rules=rules, audit=audit)
limiter = RateLimiter(registry=registry)
app = FastAPI()
@app.get("/v1/search", name="search")
async def search_route() -> dict[str, str]:
return {"endpoint": "search"}
@app.get("/v1/search-alias", name="search_alias")
async def search_alias_route() -> dict[str, str]:
return {"endpoint": "search_alias"}
@app.get("/v1/context", name="context")
async def context_route() -> dict[str, str]:
return {"endpoint": "context"}
app.add_middleware(
RateLimitMiddleware,
limiter=limiter,
route_operations={
"search": "search_operation",
"search_alias": "search_operation",
"context": "context_operation",
},
subject_resolver=_fixed_subject,
)
return app, limiter
def test_limit_cannot_be_bypassed_by_an_alternate_endpoint_for_the_same_operation() -> None:
app, _ = _app(rules={"search_operation": RateLimitRule(max_requests=2, window_seconds=60)})
client = TestClient(app)
headers = {"x-test-subject": "alice"}
assert client.get("/v1/search", headers=headers).status_code == 200
assert client.get("/v1/search-alias", headers=headers).status_code == 200
# A third call to a *different* route mapped to the *same* operation
# still hits the shared budget -- the limit cannot be dodged by calling
# whichever endpoint happens to still have quota left.
response = client.get("/v1/search", headers=headers)
assert response.status_code == 429
def test_a_429_carries_a_retry_after_header_and_a_stable_json_body() -> None:
app, _ = _app(rules={"search_operation": RateLimitRule(max_requests=1, window_seconds=60)})
client = TestClient(app)
headers = {"x-test-subject": "alice"}
client.get("/v1/search", headers=headers)
response = client.get("/v1/search", headers=headers)
assert response.status_code == 429
assert "retry-after" in response.headers
assert int(response.headers["retry-after"]) >= 1
body = response.json()
assert body["code"] == "RATE_LIMITED"
assert body["status"] == 429
assert body["retry_after_seconds"] >= 1
def test_unrelated_operations_are_not_affected_by_an_exhausted_budget() -> None:
app, _ = _app(
rules={
"search_operation": RateLimitRule(max_requests=1, window_seconds=60),
"context_operation": RateLimitRule(max_requests=1, window_seconds=60),
}
)
client = TestClient(app)
headers = {"x-test-subject": "alice"}
client.get("/v1/search", headers=headers)
assert client.get("/v1/search", headers=headers).status_code == 429
# The context operation has its own, independent budget.
assert client.get("/v1/context", headers=headers).status_code == 200
def test_different_subjects_have_independent_budgets_through_the_middleware() -> None:
app, _ = _app(rules={"search_operation": RateLimitRule(max_requests=1, window_seconds=60)})
client = TestClient(app)
assert client.get("/v1/search", headers={"x-test-subject": "alice"}).status_code == 200
assert client.get("/v1/search", headers={"x-test-subject": "alice"}).status_code == 429
assert client.get("/v1/search", headers={"x-test-subject": "bob"}).status_code == 200
def test_a_route_with_no_configured_operation_is_never_rate_limited() -> None:
app, _ = _app(rules={"search_operation": RateLimitRule(max_requests=1, window_seconds=60)})
@app.get("/unmapped")
async def unmapped_route() -> dict[str, str]:
return {"ok": "true"}
client = TestClient(app)
for _ in range(5):
assert client.get("/unmapped").status_code == 200
@@ -0,0 +1,203 @@
"""RateLimiter / RateLimitRegistry: fixed windows, retry hints, audited overrides."""
from __future__ import annotations
from collections.abc import Callable, Iterable
from datetime import UTC, datetime
from uuid import UUID, uuid4
import pytest
from ragcore.application.audit import AuditEventService
from ragcore.application.limits import (
RateLimiter,
RateLimitExceededError,
RateLimitRegistry,
RateLimitRule,
)
from ragcore.domain.audit import AuditEvent, AuditEventType
WORKSPACE_ID = UUID("00000000-0000-0000-0000-000000000001")
class _InMemoryAuditRepository:
def __init__(self) -> None:
self.events: list[AuditEvent] = []
async def append(self, event: AuditEvent) -> None:
self.events.append(event)
async def list_events(
self,
workspace_id: UUID,
*,
event_types: Iterable[AuditEventType] = (),
actor_id: UUID | None = None,
application_id: UUID | None = None,
before: datetime | None = None,
limit: int = 100,
) -> tuple[AuditEvent, ...]:
return tuple(self.events)
def _clock(value: dict[str, float]) -> Callable[[], float]:
return lambda: value["now"]
def _registry(
*, default_rules: dict[str, RateLimitRule] | None = None
) -> tuple[RateLimitRegistry, _InMemoryAuditRepository]:
repository = _InMemoryAuditRepository()
audit = AuditEventService(repository, clock=lambda: datetime.now(UTC))
return RateLimitRegistry(default_rules=default_rules or {}, audit=audit), repository
def test_no_rule_configured_means_unlimited() -> None:
registry, _ = _registry()
limiter = RateLimiter(registry=registry)
for _ in range(1000):
limiter.check(subject="alice", operation="unconfigured")
def test_requests_within_the_window_are_allowed() -> None:
registry, _ = _registry(
default_rules={"search": RateLimitRule(max_requests=3, window_seconds=60)}
)
limiter = RateLimiter(registry=registry)
for _ in range(3):
limiter.check(subject="alice", operation="search")
def test_exceeding_the_window_raises_with_a_positive_retry_after() -> None:
registry, _ = _registry(
default_rules={"search": RateLimitRule(max_requests=2, window_seconds=60)}
)
limiter = RateLimiter(registry=registry)
limiter.check(subject="alice", operation="search")
limiter.check(subject="alice", operation="search")
with pytest.raises(RateLimitExceededError) as excinfo:
limiter.check(subject="alice", operation="search")
assert excinfo.value.retry_after_seconds > 0
def test_the_window_resets_after_it_elapses() -> None:
time_value = {"now": 0.0}
registry, _ = _registry(
default_rules={"search": RateLimitRule(max_requests=1, window_seconds=10)}
)
limiter = RateLimiter(registry=registry, clock=_clock(time_value))
limiter.check(subject="alice", operation="search")
with pytest.raises(RateLimitExceededError):
limiter.check(subject="alice", operation="search")
time_value["now"] = 10.1
limiter.check(subject="alice", operation="search") # a fresh window, must not raise
def test_different_subjects_have_independent_budgets() -> None:
registry, _ = _registry(
default_rules={"search": RateLimitRule(max_requests=1, window_seconds=60)}
)
limiter = RateLimiter(registry=registry)
limiter.check(subject="alice", operation="search")
limiter.check(subject="bob", operation="search") # must not raise -- separate subject
def test_different_operations_have_independent_budgets() -> None:
registry, _ = _registry(
default_rules={
"search": RateLimitRule(max_requests=1, window_seconds=60),
"upload": RateLimitRule(max_requests=1, window_seconds=60),
}
)
limiter = RateLimiter(registry=registry)
limiter.check(subject="alice", operation="search")
limiter.check(subject="alice", operation="upload") # must not raise -- separate operation
async def test_admin_override_replaces_the_default_rule_and_is_audited() -> None:
registry, repository = _registry(
default_rules={"search": RateLimitRule(max_requests=1, window_seconds=60)}
)
limiter = RateLimiter(registry=registry)
limiter.check(subject="alice", operation="search")
with pytest.raises(RateLimitExceededError):
limiter.check(subject="alice", operation="search")
await registry.set_override(
subject="alice",
operation="search",
rule=RateLimitRule(max_requests=5, window_seconds=60),
actor_id=uuid4(),
workspace_id=WORKSPACE_ID,
trace_id="trace-1",
)
limiter.check(subject="alice", operation="search") # the raised limit now allows this
assert len(repository.events) == 1
event = repository.events[0]
assert event.event_type is AuditEventType.ADMIN_ACTION
assert event.resource_type == "rate_limit_override"
assert event.details["subject"] == "alice"
assert event.details["max_requests"] == 5
async def test_override_only_affects_the_named_subject() -> None:
registry, _ = _registry(
default_rules={"search": RateLimitRule(max_requests=1, window_seconds=60)}
)
limiter = RateLimiter(registry=registry)
await registry.set_override(
subject="alice",
operation="search",
rule=RateLimitRule(max_requests=5, window_seconds=60),
actor_id=uuid4(),
workspace_id=WORKSPACE_ID,
trace_id="trace-1",
)
limiter.check(subject="bob", operation="search")
with pytest.raises(RateLimitExceededError):
limiter.check(subject="bob", operation="search") # bob still has the default limit
async def test_clear_override_restores_the_default_rule_and_is_audited() -> None:
registry, repository = _registry(
default_rules={"search": RateLimitRule(max_requests=1, window_seconds=60)}
)
await registry.set_override(
subject="alice",
operation="search",
rule=RateLimitRule(max_requests=5, window_seconds=60),
actor_id=uuid4(),
workspace_id=WORKSPACE_ID,
trace_id="trace-1",
)
await registry.clear_override(
subject="alice",
operation="search",
actor_id=uuid4(),
workspace_id=WORKSPACE_ID,
trace_id="trace-2",
)
limiter = RateLimiter(registry=registry)
limiter.check(subject="alice", operation="search")
with pytest.raises(RateLimitExceededError):
limiter.check(subject="alice", operation="search") # back to the default limit of 1
assert len(repository.events) == 2
assert repository.events[-1].details["cleared"] is True
def test_invalid_rule_construction_is_rejected() -> None:
with pytest.raises(ValueError, match="max_requests"):
RateLimitRule(max_requests=0, window_seconds=60)
with pytest.raises(ValueError, match="window_seconds"):
RateLimitRule(max_requests=1, window_seconds=0)