fix(platform): bind AI runs to canonical correlation
Some checks failed
CD Pipeline / select-latest-carrier (push) Successful in 51s
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m45s
CD Pipeline / revalidate-deploy-carrier (push) Successful in 33s
CD Pipeline / revalidate-post-deploy-carrier (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
Some checks failed
CD Pipeline / select-latest-carrier (push) Successful in 51s
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m45s
CD Pipeline / revalidate-deploy-carrier (push) Successful in 33s
CD Pipeline / revalidate-post-deploy-carrier (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
This commit is contained in:
162
apps/api/tests/integration/test_platform_run_idempotency_race.py
Normal file
162
apps/api/tests/integration/test_platform_run_idempotency_race.py
Normal file
@@ -0,0 +1,162 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import make_url, text
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.pool import NullPool
|
||||
|
||||
from src.services import platform_runtime
|
||||
|
||||
_TEST_DATABASE_ENV = "AWOOOI_RUNTIME_IDENTITY_TEST_DATABASE_URL"
|
||||
_TEST_DATABASE_NAME = "awoooi_runtime_identity_test"
|
||||
_LOCAL_DATABASE_HOSTS = frozenset({"127.0.0.1", "localhost"})
|
||||
|
||||
|
||||
def _validated_test_database_url() -> str:
|
||||
raw_url = os.getenv(_TEST_DATABASE_ENV, "").strip()
|
||||
if not raw_url:
|
||||
pytest.skip(f"{_TEST_DATABASE_ENV} is required for the real PostgreSQL race test")
|
||||
|
||||
parsed = make_url(raw_url)
|
||||
if (
|
||||
parsed.get_backend_name() != "postgresql"
|
||||
or parsed.get_driver_name() != "asyncpg"
|
||||
or parsed.host not in _LOCAL_DATABASE_HOSTS
|
||||
or parsed.database != _TEST_DATABASE_NAME
|
||||
):
|
||||
pytest.fail(
|
||||
f"{_TEST_DATABASE_ENV} must use postgresql+asyncpg on localhost "
|
||||
f"with database {_TEST_DATABASE_NAME!r}"
|
||||
)
|
||||
return raw_url
|
||||
|
||||
|
||||
_RUN_STATE_DDL = """
|
||||
CREATE TABLE awooop_run_state (
|
||||
run_id UUID PRIMARY KEY,
|
||||
project_id VARCHAR(64) NOT NULL,
|
||||
agent_id VARCHAR(128) NOT NULL,
|
||||
state VARCHAR(32) NOT NULL DEFAULT 'pending',
|
||||
lease_until TIMESTAMP,
|
||||
next_attempt_at TIMESTAMP,
|
||||
heartbeat_at TIMESTAMP,
|
||||
worker_id VARCHAR(128),
|
||||
attempt_count SMALLINT NOT NULL DEFAULT 0,
|
||||
max_attempts SMALLINT NOT NULL DEFAULT 3,
|
||||
trace_id VARCHAR(128),
|
||||
trigger_type VARCHAR(32),
|
||||
trigger_ref VARCHAR(256),
|
||||
is_shadow BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
input_sha256 VARCHAR(64),
|
||||
output_sha256 VARCHAR(64),
|
||||
cost_usd NUMERIC(10, 4) NOT NULL DEFAULT 0,
|
||||
step_count SMALLINT NOT NULL DEFAULT 0,
|
||||
error_code VARCHAR(64),
|
||||
error_detail TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
started_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
timeout_at TIMESTAMP
|
||||
)
|
||||
"""
|
||||
|
||||
_RUN_IDEMPOTENCY_DDL = """
|
||||
CREATE TABLE awooop_run_idempotency (
|
||||
idempotency_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id VARCHAR(64) NOT NULL,
|
||||
channel_type VARCHAR(32) NOT NULL,
|
||||
provider_event_id VARCHAR(256) NOT NULL,
|
||||
run_id UUID NOT NULL REFERENCES awooop_run_state(run_id),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uix_run_idempotency_key
|
||||
UNIQUE (project_id, channel_type, provider_event_id)
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_provider_event_has_one_postgresql_winner(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
engine = create_async_engine(_validated_test_database_url(), poolclass=NullPool)
|
||||
factory = async_sessionmaker(engine, expire_on_commit=False, autoflush=False)
|
||||
tables_created = False
|
||||
|
||||
@asynccontextmanager
|
||||
async def test_db_context(project_id: str):
|
||||
assert project_id == "awoooi"
|
||||
async with factory() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
async def skip_redis_cache(*_args: object) -> None:
|
||||
return None
|
||||
|
||||
try:
|
||||
async with engine.begin() as connection:
|
||||
existing_tables = (
|
||||
await connection.execute(
|
||||
text(
|
||||
"SELECT to_regclass('awooop_run_state'), "
|
||||
"to_regclass('awooop_run_idempotency')"
|
||||
)
|
||||
)
|
||||
).one()
|
||||
if any(existing_tables):
|
||||
pytest.fail(
|
||||
"dedicated runtime identity test database must not contain "
|
||||
"pre-existing run tables"
|
||||
)
|
||||
await connection.execute(text(_RUN_STATE_DDL))
|
||||
await connection.execute(text(_RUN_IDEMPOTENCY_DDL))
|
||||
tables_created = True
|
||||
|
||||
monkeypatch.setattr(platform_runtime, "get_db_context", test_db_context)
|
||||
monkeypatch.setattr(platform_runtime, "_cache_idempotency", skip_redis_cache)
|
||||
|
||||
first, second = await asyncio.gather(
|
||||
platform_runtime.create_run(
|
||||
project_id="awoooi",
|
||||
agent_id="runtime-agent",
|
||||
trigger_type="channel_event",
|
||||
channel_type="telegram",
|
||||
provider_event_id="same-real-postgresql-event",
|
||||
),
|
||||
platform_runtime.create_run(
|
||||
project_id="awoooi",
|
||||
agent_id="runtime-agent",
|
||||
trigger_type="channel_event",
|
||||
channel_type="telegram",
|
||||
provider_event_id="same-real-postgresql-event",
|
||||
),
|
||||
)
|
||||
|
||||
async with engine.connect() as connection:
|
||||
run_count = await connection.scalar(
|
||||
text("SELECT COUNT(*) FROM awooop_run_state")
|
||||
)
|
||||
mapping_count = await connection.scalar(
|
||||
text("SELECT COUNT(*) FROM awooop_run_idempotency")
|
||||
)
|
||||
durable_run_id = await connection.scalar(
|
||||
text("SELECT run_id FROM awooop_run_idempotency")
|
||||
)
|
||||
|
||||
assert first[0] == second[0] == durable_run_id
|
||||
assert sorted((first[1], second[1])) == [False, True]
|
||||
assert run_count == 1
|
||||
assert mapping_count == 1
|
||||
finally:
|
||||
if tables_created:
|
||||
async with engine.begin() as connection:
|
||||
await connection.execute(text("DROP TABLE awooop_run_idempotency"))
|
||||
await connection.execute(text("DROP TABLE awooop_run_state"))
|
||||
await engine.dispose()
|
||||
Reference in New Issue
Block a user