PropertyDamage.Model.Projection.Liveness (PropertyDamage v0.2.0)
View SourceProjection that tracks pending operations and asserts progress (liveness).
Traditional SPBT focuses on safety properties - invariants that must never be violated. Liveness properties are different: they guarantee that something good eventually happens.
Safety vs Liveness
| Property Type | Example | Detection |
|---|---|---|
| Safety | "Balance never goes negative" | State assertion |
| Liveness | "Every request eventually completes" | Timeout on pending |
What This Projection Detects
- Deadlock - System hangs forever, no invariant violated
- Livelock - System is busy but makes no useful progress
- Starvation - One client always succeeds, another never does
- Infinite retry loops - System keeps retrying without bound
How It Works
- Track commands that start operations (e.g., CreateTransfer)
- Track events that complete operations (e.g., TransferCompleted, TransferFailed)
- Periodically check for operations pending "too long"
- Report stuck operations as liveness violations
Configuration
defmodule MyModel do
def assertion_projections do
[
{PropertyDamage.Model.Projection.Liveness, [
max_pending_duration_ms: 10_000,
required_completions: %{
CreateTransfer => [TransferCompleted, TransferFailed],
CreateOrder => [OrderConfirmed, OrderRejected]
}
]}
]
end
endOptions
:max_pending_duration_ms- How long before an operation is "stuck" (default: 10_000):required_completions- Map of command module => list of completion event modules:check_interval- How often to check (in step count, default: 10)
Limitations
- Requires wall-clock time, complicating deterministic replay
- Timeout thresholds are arbitrary
- Can't detect "infinite but slow progress" (livelock with occasional success)
Summary
Functions
Check for stuck operations.
Get operations that have been pending longer than the specified milliseconds.
Get count of currently pending operations.
Get list of pending operation details.
Types
@type pending_operation() :: %{ command_module: module(), started_at: integer(), command_index: non_neg_integer(), expected_completions: [module()] }
@type t() :: %PropertyDamage.Model.Projection.Liveness{ check_interval: pos_integer(), current_step: non_neg_integer(), max_pending_duration_ms: pos_integer(), pending_operations: %{required(reference()) => pending_operation()}, required_completions: %{required(module()) => [module()]} }
Functions
Check for stuck operations.
Returns :ok if no operations have been pending too long.
Returns {:error, reason} if operations appear stuck.
@spec operations_pending_longer_than(t(), pos_integer()) :: [pending_operation()]
Get operations that have been pending longer than the specified milliseconds.
@spec pending_count(t()) :: non_neg_integer()
Get count of currently pending operations.
@spec pending_operations(t()) :: [pending_operation()]
Get list of pending operation details.