A distributed lock powered by the justalock service
Project description
justalock-client
A Python client library for the justalock distributed lock service. This library provides distributed locking functionality to coordinate work across multiple processes or services using Python's async/await patterns and context managers.
Features
- 🔒 Distributed Locking: Coordinate access to shared resources across multiple processes
- 🔄 Automatic Lock Refresh: Keeps locks alive while your work is running
- 🎯 Async/Await Support: Built for modern Python with full asyncio integration
- 📦 Zero Dependencies: Built using only Python standard library
- 🐍 Context Managers: Clean resource management with
async with
Installation
pip install justalock-client
Requirements
- Python 3.11 or higher
- No external dependencies (uses only Python standard library)
Quick Start
import asyncio
from justalock_client import Lock, generate_random_lock_id
async def main():
# Create a lock with a unique identifier
lock = Lock.builder(generate_random_lock_id()).build()
# Use the lock as a context manager
async with lock:
# Your critical work here - only runs when you have the lock
print("I have the lock! Doing important work...")
await asyncio.sleep(1)
print("Work completed!")
# Run the async function
asyncio.run(main())
How It Works
- Lock Acquisition: The context manager attempts to acquire the specified lock, retrying until successful
- Automatic Refresh: The lock is automatically refreshed in the background to maintain ownership
- Context Management: The
async withstatement ensures proper cleanup when exiting the block - Lock Loss Detection: If the lock is lost (due to network issues, expiration, or being stolen by another client), the lock object signals this through the
is_lock_lostproperty - Cleanup: The context manager automatically stops the refresh process when exiting
Thread Safety
- Lock instances are safe for use within a single asyncio event loop
- Do not share Lock instances across different event loops or threads
- Each async context (
async with lock:) creates a new lock session
Performance Notes
- Lock instances can be reused for multiple lock sessions
- The HTTP client uses connection pooling internally via urllib
- Refresh operations are performed in background tasks and don't block your code
Usage Examples
Basic Usage with Custom Configuration
import asyncio
from justalock_client import Lock
async def process_data():
lock = (Lock.builder("data-processing-lock")
.client_id("worker-1")
.lifetime_seconds(300) # 5 minutes
.build())
async with lock:
print("Processing data...")
# Only one worker will process data at a time
await simulate_data_processing()
print("Data processing complete!")
async def simulate_data_processing():
await asyncio.sleep(2)
asyncio.run(process_data())
Monitoring Lock Status
import asyncio
from justalock_client import Lock
async def monitored_work():
lock = Lock.builder("monitored-lock").build()
async with lock:
# Start work and monitor for lock loss
work_task = asyncio.create_task(long_running_work())
monitor_task = asyncio.create_task(lock.wait_for_lock_lost())
# Wait for either work completion or lock loss
done, pending = await asyncio.wait(
[work_task, monitor_task],
return_when=asyncio.FIRST_COMPLETED
)
# Cancel pending tasks
for task in pending:
task.cancel()
if work_task in done:
print("Work completed successfully!")
return await work_task
else:
print("Lock was lost, stopping work!")
return None
async def long_running_work():
for i in range(10):
print(f"Working... step {i+1}/10")
await asyncio.sleep(1)
return "work complete"
asyncio.run(monitored_work())
Service Coordination
import asyncio
from justalock_client import Lock
# Service A - Database migration
async def migrate_database():
lock = (Lock.builder("db-migration-lock")
.client_id("migration-service")
.lifetime_seconds(1800) # 30 minutes
.build())
async with lock:
print("Starting database migration...")
await perform_migration()
print("Migration completed!")
# Service B - Cache warming (waits for migration)
async def warm_cache():
lock = (Lock.builder("db-migration-lock") # Same lock ID
.client_id("cache-service")
.lifetime_seconds(300)
.build())
async with lock:
print("Migration complete, warming cache...")
await warm_application_cache()
print("Cache warmed!")
async def perform_migration():
# Simulate long-running migration
await asyncio.sleep(5)
async def warm_application_cache():
# Simulate cache warming
await asyncio.sleep(2)
# Run services (in practice, these would be separate processes)
async def main():
await asyncio.gather(
migrate_database(),
warm_cache()
)
asyncio.run(main())
Error Handling
import asyncio
from justalock_client import Lock, JustalockError
async def robust_work():
lock = Lock.builder("error-prone-work").build()
try:
async with lock:
print("Starting work...")
# Simulate work that might fail
await risky_operation()
print("Work completed successfully!")
except JustalockError as e:
print(f"Lock error: {e}")
except Exception as e:
print(f"Work failed: {e}")
async def risky_operation():
# Simulate operation that might fail
import random
if random.random() < 0.3:
raise Exception("Operation failed!")
await asyncio.sleep(1)
asyncio.run(robust_work())
Convenience Function
import asyncio
from justalock_client import with_lock
async def my_work():
print("Doing important work...")
await asyncio.sleep(1)
return "work result"
async def main():
# Use the convenience function for simple cases
result = await with_lock(
"simple-lock",
my_work,
lifetime_seconds=60
)
print(f"Result: {result}")
asyncio.run(main())
Multiple Lock ID Formats
import asyncio
from justalock_client import Lock, generate_random_lock_id
async def test_lock_formats():
# String lock IDs
lock1 = Lock.builder("simple-string").build()
# Integer lock IDs
lock2 = Lock.builder(12345).build()
# Binary lock IDs
lock3 = Lock.builder(b"binary-lock-id").build()
# UUID lock IDs
lock4 = Lock.builder("550e8400-e29b-41d4-a716-446655440000").build()
# Random lock IDs
random_id = generate_random_lock_id()
lock5 = Lock.builder(random_id).build()
# Test all formats work
for i, lock in enumerate([lock1, lock2, lock3, lock4, lock5], 1):
async with lock:
print(f"Lock {i} acquired successfully!")
await asyncio.sleep(0.1)
asyncio.run(test_lock_formats())
Periodic Tasks
import asyncio
from justalock_client import Lock
async def periodic_cleanup():
"""Only one instance performs cleanup across all servers."""
lock = (Lock.builder("daily-cleanup-lock")
.client_id(f"cleanup-{os.getpid()}")
.lifetime_seconds(3600) # 1 hour max
.build())
try:
async with lock:
print("Starting periodic cleanup...")
await cleanup_old_files()
await vacuum_database()
print("Cleanup completed!")
return True
except JustalockError:
print("Another instance is performing cleanup")
return False
async def cleanup_old_files():
await asyncio.sleep(1) # Simulate cleanup
async def vacuum_database():
await asyncio.sleep(1) # Simulate vacuum
# Schedule this to run periodically
async def main():
while True:
await periodic_cleanup()
await asyncio.sleep(86400) # Wait 24 hours
# In practice, you'd use a proper scheduler
# asyncio.run(main())
API Reference
Lock.builder(lock_id)
Creates a LockBuilder for configuring a lock.
Parameters:
lock_id(str, int, or bytes): Lock identifier
Returns: LockBuilder instance
LockBuilder Methods
.client_id(client_id)
Set a custom client identifier.
client_id(str or bytes): Client identifier
.lifetime_seconds(seconds)
Set lock lifetime in seconds (1-65535).
seconds(int): Lock lifetime
.refresh_interval_seconds(seconds)
Set refresh interval in seconds.
seconds(float): Refresh interval
.base_url(url)
Set custom service URL (for testing or custom deployments).
url(str): Service URL
.build()
Build the configured Lock instance.
Returns: Lock instance
Lock Methods
async with lock:
Use the lock as an async context manager. Automatically acquires the lock on entry and releases it on exit.
.is_lock_lost
Property that returns True if the lock has been lost.
await lock.wait_for_lock_lost()
Wait until the lock is lost (useful for monitoring).
Development
Testing
The library includes comprehensive tests with a mock server:
# Run tests
python -m pytest test_justalock_client.py -v
# Or using unittest
python test_justalock_client.py
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file justalock_client-0.1.0.tar.gz.
File metadata
- Download URL: justalock_client-0.1.0.tar.gz
- Upload date:
- Size: 14.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
07ba02d529dd77ce0329fdb07e3990d1396886b4fe5dd3c24f588f7161c8f8d9
|
|
| MD5 |
156728bd33c51ddba769bc3f45edd5a7
|
|
| BLAKE2b-256 |
b9f0d555b9300ad3e716e18835505792726002b60f64f51f3ab5ffd9b702be0b
|
File details
Details for the file justalock_client-0.1.0-py3-none-any.whl.
File metadata
- Download URL: justalock_client-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2c7b8f76f9d9e66f47f1c235a7565cbe7322dcfa228c4ec841bf702e322c6db5
|
|
| MD5 |
df673237ab04b259e228b1b9fbc88c04
|
|
| BLAKE2b-256 |
e6778c5572e2241808097ec4c0add1dd26652e968cca7598de9f6579cc9eff2e
|