Files
cleveragents-core/features/concurrency.feature
T
freemo 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
feat(concurrency): add plan and project locks
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
2026-02-25 10:48:05 -05:00

140 lines
4.9 KiB
Gherkin

Feature: Concurrency Locks
As a developer
I want plan-level and project-level advisory locks
So that concurrent modifications to shared resources are prevented
Background:
Given I have a lock service backed by an in-memory database
# --- Acquisition ---
Scenario: Acquire a plan lock
When I acquire a lock on plan "plan-001" as owner "session-a"
Then the lock should be acquired successfully
Scenario: Acquire a project lock
When I acquire a lock on project "local/my-proj" as owner "session-a"
Then the lock should be acquired successfully
# --- Re-entrant acquisition ---
Scenario: Same owner re-acquires a plan lock
Given owner "session-a" holds a lock on plan "plan-001"
When I acquire a lock on plan "plan-001" as owner "session-a"
Then the lock should be acquired successfully
# --- Conflict detection ---
Scenario: Different owner is rejected with LockConflictError
Given owner "session-a" holds a lock on plan "plan-001"
When I try to acquire a lock on plan "plan-001" as owner "session-b"
Then a lock conflict error should be raised for owner "session-a"
# --- Release ---
Scenario: Release a held lock
Given owner "session-a" holds a lock on plan "plan-001"
When I release the lock on plan "plan-001" as owner "session-a"
Then the lock should be released successfully
Scenario: Release returns false for non-existent lock
When I release the lock on plan "plan-999" as owner "nobody"
Then the release result should be false
# --- Renewal ---
Scenario: Renew a held lock
Given owner "session-a" holds a lock on plan "plan-001"
When I renew the lock on plan "plan-001" as owner "session-a"
Then the lock renewal should succeed
Scenario: Renew an expired lock raises LockExpiredError
Given owner "session-a" held an expired lock on plan "plan-001"
When I try to renew the expired lock on plan "plan-001" as owner "session-a"
Then a lock expired error should be raised
# --- Expiry and cleanup ---
Scenario: Expired lock is replaced by a new owner
Given owner "session-a" held an expired lock on plan "plan-001"
When I acquire a lock on plan "plan-001" as owner "session-b"
Then the lock should be acquired successfully
Scenario: Cleanup expired locks on startup
Given there are 3 expired locks in the database
When I run cleanup expired locks
Then the expired lock count should be 3
# --- Graceful shutdown ---
Scenario: Release all locks for an owner on shutdown
Given owner "session-a" holds locks on plans "p1" and "p2" and project "proj-1"
When I release all locks for owner "session-a"
Then the released count should be 3
# --- Diagnostics ---
Scenario: Count stale locks for diagnostics
Given there are 2 expired locks in the database
When I count stale locks
Then the stale lock count should be 2
# --- is_locked check ---
Scenario: is_locked returns true for active lock
Given owner "session-a" holds a lock on plan "plan-001"
When I check if plan "plan-001" is locked
Then the resource should be locked
Scenario: is_locked returns false when no lock exists
When I check if plan "plan-999" is locked
Then the resource should not be locked
# --- Validation ---
Scenario: Reject empty owner_id
When I try to acquire a lock with empty owner_id
Then a lock validation error should be raised
Scenario: Reject invalid resource type
When I try to acquire a lock with invalid resource type "widget"
Then a lock validation error should be raised
Scenario: Reject empty resource_id
When I try to acquire a lock with empty resource_id
Then a lock validation error should be raised
Scenario: Reject TTL below minimum
When I try to acquire a lock with TTL 2
Then a lock validation error should be raised
Scenario: Reject TTL above maximum
When I try to acquire a lock with TTL 7200
Then a lock validation error should be raised
# --- Additional validation ---
Scenario: Reject None session_factory
When I try to create a lock service with None session factory
Then a lock validation error should be raised
Scenario: Reject empty resource_type on acquire
When I try to acquire a lock with empty resource type
Then a lock validation error should be raised
Scenario: Reject empty owner_id on release
When I try to release a lock with empty owner_id
Then a lock validation error should be raised
Scenario: Reject empty owner_id on renew
When I try to renew a lock with empty owner_id
Then a lock validation error should be raised
Scenario: Reject empty owner_id on release_all_for_owner
When I try to release all locks with empty owner_id
Then a lock validation error should be raised
Scenario: Renew returns false for nonexistent lock
When I try to renew a lock that does not exist
Then the renew result should be false