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
140 lines
4.9 KiB
Gherkin
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
|