A clean, educational Python implementation of the CASPaxos consensus protocol. CASPaxos is a replicated state machine protocol that provides strongly consistent operations without the complexity of leader election and log replication.
- Simple API: Core
change(key, update_fn)operation for all state modifications - No Leader Election: Symmetric peer-to-peer design avoids single points of failure
- Optimistic Concurrency: Compare-and-swap operations for conflict resolution
- Modular Design: Separate acceptor, proposer, and storage components
- Async/Await: Built on Python's asyncio for concurrent operations
- In-Memory Storage: Default in-memory acceptors for development and testing
- Thread-Safe: Per-key locking ensures consistency
The implementation is self-contained with no external dependencies for the core functionality:
# No installation needed, just import from the caspaxos directory
from caspaxos import CASPaxos, KeyValueStore, CASKeyValueimport asyncio
from caspaxos import CASPaxos
async def main():
# Create CASPaxos instance with 3 acceptors (tolerates 1 failure)
paxos = CASPaxos(node_id="node1", num_acceptors=3)
# Core operation: change(key, update_function)
# Set initial value
await paxos.change("counter", lambda x: 0)
# Increment counter
result = await paxos.change("counter", lambda x: x + 1)
print(f"Counter: {result}") # Counter: 1
# Convenience methods
await paxos.put("key1", "value1")
value = await paxos.get("key1")
print(f"Value: {value}") # Value: value1
asyncio.run(main())from caspaxos import KeyValueStore
async def main():
kv = KeyValueStore()
# Store data
await kv.put("user:1", {"name": "Alice", "age": 30})
# Retrieve data
user = await kv.get("user:1")
print(user) # {'name': 'Alice', 'age': 30}
# Delete data
await kv.delete("user:1")
asyncio.run(main())from caspaxos import CASKeyValue
async def main():
cas = CASKeyValue()
# Versioned updates
doc = await cas.read("doc1")
print(f"Version: {doc['version']}") # Version: 0
# Update with version check
await cas.write("doc1", 0, "content v1")
# Atomic counter
count = await cas.increment("views")
print(f"Views: {count}") # Views: 1
asyncio.run(main())-
BallotNumber (
core/ballot.py)- Totally ordered ballot numbers using (counter, node_id) pairs
- Ensures global ordering of proposals
-
Proposer (
core/proposer.py)- Coordinates the two-phase consensus protocol
- Manages prepare and accept phases
- Maintains per-key locks for serialization
-
Acceptor (
storage/acceptor.py)- Stores promised and accepted values
- Responds to prepare and accept requests
- In-memory and Redis implementations available
-
Messages (
core/messages.py)- PrepareRequest/Response
- AcceptRequest/Response
Client -> Proposer -> Acceptors
| |
|-- Phase 1: Prepare -->
|<-- Promise + Value ---
| |
|-- Phase 2: Accept --->
|<-- Accepted ---------
|
Result -> Client
class DistributedLock:
def __init__(self, paxos, lock_name):
self.paxos = paxos
self.lock_name = f"lock:{lock_name}"
async def acquire(self, owner):
def try_acquire(current):
if current is None:
return {"owner": owner, "time": time.time()}
raise ValueError("Lock held")
try:
await self.paxos.change(self.lock_name, try_acquire)
return True
except:
return Falseasync def concurrent_increments():
paxos = CASPaxos(num_acceptors=3)
await paxos.put("counter", 0)
async def increment():
for _ in range(100):
await paxos.change("counter", lambda x: x + 1)
# Run 10 workers concurrently
await asyncio.gather(*[increment() for _ in range(10)])
# Result will be exactly 1000
final = await paxos.get("counter")
assert final == 1000A Makefile is provided for convenience:
# Show all available targets
make help
# Run all tests
make test
# Run tests with verbose output
make test-verbose
# Run specific test suites
make test-ballot # Test ballot number implementation
make test-acceptor # Test acceptor implementation
make test-proposer # Test proposer implementation
make test-caspaxos # Test main CASPaxos API
# Run examples
make example # or 'make demo'
# Run everything (tests + examples)
make all
# Clean cache files
make clean# Set Python path
export PYTHONPATH=/app
# Run all tests
env/bin/python -m pytest caspaxos/caspaxos/__tests__/
# Run specific test file
env/bin/python -m pytest caspaxos/caspaxos/__tests__/test_ballot.py
# Run with coverage
env/bin/python -m pytest --cov=caspaxos caspaxos/caspaxos/__tests__/# Using make
make example
# Or manually
PYTHONPATH=/app env/bin/python -m caspaxos.caspaxos.example- Python Async: Uses asyncio instead of JavaScript promises
- Type Hints: Full type annotations for better IDE support
- Dataclasses: Message types use Python dataclasses
- Testing: pytest-based test suite
- Memory: All acceptor state is in-memory by default
- Concurrency: Per-key locking allows parallel operations on different keys
- Caching: Proposers cache accepted values to avoid unnecessary prepare phases
- Quorum: Default majority quorum (N/2 + 1) for N acceptors
- Single Machine: Current implementation runs on a single machine
- No Persistence: In-memory acceptors lose state on restart
- No Network: Acceptors communicate via method calls, not network
- No Reconfiguration: Static acceptor membership
- Redis Backend: Persistent acceptor state using Redis
- Network Communication: TCP/HTTP based acceptor protocol
- Cluster Membership: Dynamic addition/removal of acceptors
- Monitoring: Metrics and observability
- Optimizations: Batching, pipelining, and lease-based reads
See LICENSE
This is an educational implementation. Contributions that improve clarity, correctness, or educational value are welcome!
- Write tests for new features
- Maintain clean, readable code
- Add docstrings and type hints
- Update documentation