7a298ede6e
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 16s
CI / quality (pull_request) Successful in 30s
CI / build (pull_request) Successful in 24s
CI / security (pull_request) Successful in 40s
CI / typecheck (pull_request) Successful in 1m0s
CI / integration_tests (pull_request) Successful in 4m14s
CI / unit_tests (pull_request) Successful in 16m25s
CI / docker (pull_request) Successful in 55s
CI / benchmark-regression (pull_request) Successful in 22m55s
CI / coverage (pull_request) Successful in 38m4s
CI / lint (push) Successful in 13s
CI / build (push) Successful in 15s
CI / quality (push) Successful in 28s
CI / typecheck (push) Successful in 31s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 31s
CI / integration_tests (push) Successful in 3m19s
CI / benchmark-publish (push) Successful in 14m23s
CI / unit_tests (push) Successful in 14m44s
CI / docker (push) Successful in 38s
CI / coverage (push) Successful in 32m22s
Implemented plan-level and project-level locking with configurable timeouts. Added locks table via Alembic migration storing owner_id, resource_type, resource_id, acquired_at, and expires_at. Locks enforced in PlanLifecycleService transitions. Support for re-entrant acquisition, lock renewal, graceful shutdown release, and startup cleanup of expired locks. Added diagnostics check for stale lock reporting. ISSUES CLOSED: #327
136 lines
3.7 KiB
Python
136 lines
3.7 KiB
Python
"""Helper script for concurrency_locks.robot smoke test."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from datetime import UTC, datetime, timedelta
|
|
from typing import Any
|
|
|
|
sys.path.insert(0, "src")
|
|
sys.path.insert(0, ".")
|
|
|
|
from sqlalchemy import create_engine, event
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
from cleveragents.application.services.lock_service import LockService
|
|
from cleveragents.core.exceptions import LockConflictError, LockExpiredError
|
|
from cleveragents.infrastructure.database.models import Base, LockModel
|
|
|
|
|
|
def main() -> None:
|
|
engine = create_engine(
|
|
"sqlite:///:memory:",
|
|
echo=False,
|
|
future=True,
|
|
connect_args={"check_same_thread": False},
|
|
)
|
|
|
|
@event.listens_for(engine, "connect")
|
|
def _fk(dbapi_conn: Any, _rec: Any) -> None:
|
|
cursor = dbapi_conn.cursor()
|
|
cursor.execute("PRAGMA foreign_keys=ON")
|
|
cursor.close()
|
|
|
|
Base.metadata.create_all(engine)
|
|
sf: sessionmaker[Session] = sessionmaker(
|
|
bind=engine,
|
|
expire_on_commit=False,
|
|
autoflush=False,
|
|
autocommit=False,
|
|
class_=Session,
|
|
)
|
|
|
|
svc = LockService(session_factory=sf)
|
|
|
|
# Test 1: basic acquire
|
|
assert svc.acquire("owner-1", "plan", "p1") is True
|
|
print("PASS: acquire")
|
|
|
|
# Test 2: re-entrant
|
|
assert svc.acquire("owner-1", "plan", "p1") is True
|
|
print("PASS: re-entrant acquire")
|
|
|
|
# Test 3: conflict
|
|
try:
|
|
svc.acquire("owner-2", "plan", "p1")
|
|
print("FAIL: expected LockConflictError")
|
|
sys.exit(1)
|
|
except LockConflictError:
|
|
print("PASS: conflict detection")
|
|
|
|
# Test 4: release
|
|
assert svc.release("owner-1", "plan", "p1") is True
|
|
print("PASS: release")
|
|
|
|
# Test 5: acquire after release
|
|
assert svc.acquire("owner-2", "plan", "p1") is True
|
|
print("PASS: acquire after release")
|
|
|
|
# Test 6: renew
|
|
assert svc.renew("owner-2", "plan", "p1") is True
|
|
print("PASS: renew")
|
|
|
|
# Test 7: release_all_for_owner
|
|
svc.acquire("owner-3", "plan", "p10")
|
|
svc.acquire("owner-3", "project", "proj-1")
|
|
count = svc.release_all_for_owner("owner-3")
|
|
assert count == 2, f"Expected 2, got {count}"
|
|
print("PASS: release_all_for_owner")
|
|
|
|
# Test 8: cleanup_expired
|
|
now = datetime.now(tz=UTC)
|
|
expired = (now - timedelta(seconds=60)).isoformat()
|
|
acquired = (now - timedelta(seconds=120)).isoformat()
|
|
session = sf()
|
|
session.add(
|
|
LockModel(
|
|
owner_id="old",
|
|
resource_type="plan",
|
|
resource_id="stale-1",
|
|
acquired_at=acquired,
|
|
expires_at=expired,
|
|
)
|
|
)
|
|
session.commit()
|
|
session.close()
|
|
|
|
cleaned = svc.cleanup_expired()
|
|
assert cleaned >= 1, f"Expected >=1 cleaned, got {cleaned}"
|
|
print("PASS: cleanup_expired")
|
|
|
|
# Test 9: count_stale_locks returns 0 after cleanup
|
|
assert svc.count_stale_locks() == 0
|
|
print("PASS: count_stale_locks")
|
|
|
|
# Test 10: is_locked
|
|
svc.acquire("owner-4", "plan", "check-me")
|
|
assert svc.is_locked("plan", "check-me") is True
|
|
assert svc.is_locked("plan", "not-locked") is False
|
|
print("PASS: is_locked")
|
|
|
|
# Test 11: renew expired raises LockExpiredError
|
|
session2 = sf()
|
|
session2.add(
|
|
LockModel(
|
|
owner_id="owner-5",
|
|
resource_type="plan",
|
|
resource_id="expired-renew",
|
|
acquired_at=acquired,
|
|
expires_at=expired,
|
|
)
|
|
)
|
|
session2.commit()
|
|
session2.close()
|
|
try:
|
|
svc.renew("owner-5", "plan", "expired-renew")
|
|
print("FAIL: expected LockExpiredError")
|
|
sys.exit(1)
|
|
except LockExpiredError:
|
|
print("PASS: renew expired raises error")
|
|
|
|
print("\nPASS: concurrency_locks smoke test")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|