Client-side load balancing for ScyllaDB Alternator
Project description
Alternator Load Balancing Client for Python
A Python library that provides client-side load balancing for ScyllaDB Alternator, wrapping boto3/aioboto3 to transparently distribute requests across cluster nodes.
Features
- Automatic Load Balancing: Distributes requests across all available Alternator nodes using round-robin selection
- Node Discovery: Automatically discovers cluster topology via the
/localnodesendpoint - Topology Awareness: Route requests to specific datacenters or racks
- Key Affinity Routing: Optimizes LWT (Lightweight Transaction) operations by routing requests for the same partition key to the same node
- Request Compression: Optional gzip compression to reduce bandwidth
- Header Optimization: Filters unnecessary headers to reduce request overhead
- TLS Support: Full TLS/SSL support with custom CA certificates
- Async Support: Full async/await support via aioboto3
Installation
# Basic installation (sync client only)
pip install alternator-client
# With async support
pip install alternator-client[async]
Note: The PyPI package name is
alternator-client, but the Python import remainsalternator.
Quick Start
Synchronous Client
from alternator import AlternatorConfig, AlternatorClient
# Configure the client
config = AlternatorConfig(
seed_hosts=["192.168.1.1", "192.168.1.2"],
port=8000,
)
# Use as a context manager (recommended)
with AlternatorClient(config) as client:
# Use like a normal boto3 DynamoDB client
response = client.list_tables()
print(response["TableNames"])
# Put an item
client.put_item(
TableName="my_table",
Item={
"pk": {"S": "user123"},
"data": {"S": "Hello, World!"},
}
)
Asynchronous Client
import asyncio
from alternator import AlternatorConfig
from alternator.async_client import AsyncAlternatorClient
async def main():
config = AlternatorConfig(
seed_hosts=["192.168.1.1"],
port=8000,
)
async with AsyncAlternatorClient(config) as client:
# Use like a normal aioboto3 DynamoDB client
response = await client.list_tables()
print(response["TableNames"])
asyncio.run(main())
Configuration
Basic Configuration
from alternator import AlternatorConfig
config = AlternatorConfig(
seed_hosts=["node1.example.com", "node2.example.com"],
port=8000,
scheme="http", # or "https" for TLS
)
Using the Builder Pattern
from alternator import (
AlternatorConfigBuilder,
CompressionAlgorithm,
KeyRouteAffinityMode,
TlsConfig,
)
config = (
AlternatorConfigBuilder()
.with_seeds("node1.example.com", "node2.example.com")
.with_port(8000)
.with_https(TlsConfig.system_default())
.with_datacenter("us-east-1")
.with_compression(CompressionAlgorithm.GZIP, min_size=1024)
.with_key_affinity(KeyRouteAffinityMode.RMW)
.with_refresh_intervals(active_ms=1000, idle_ms=60000)
.build()
)
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
seed_hosts |
Sequence[str] |
(required) | Initial nodes for cluster discovery |
port |
int |
(required) | Alternator port |
scheme |
str |
"http" |
Protocol scheme ("http" or "https") |
routing_scope |
RoutingScope |
ClusterScope() |
Topology-aware routing |
compression |
CompressionAlgorithm |
NONE |
Request compression |
min_compression_size_bytes |
int |
1024 |
Minimum body size to compress |
optimize_headers |
bool |
False |
Enable header filtering |
headers_whitelist |
frozenset[str] |
None |
Additional headers to keep |
authentication_enabled |
bool |
True |
Include auth headers |
tls |
TlsConfig |
system default | TLS configuration |
key_affinity |
KeyRouteAffinityConfig |
NONE |
Key-based routing |
max_pool_connections |
int |
200 |
Max connections per host |
active_refresh_interval_ms |
int |
1000 |
Node refresh interval when active |
idle_refresh_interval_ms |
int |
60000 |
Node refresh interval when idle |
Routing Scopes
Control which nodes receive your requests based on topology:
from alternator import ClusterScope, DatacenterScope, RackScope
# Route to any node in the cluster (default)
config = AlternatorConfig(
seed_hosts=["node1"],
port=8000,
routing_scope=ClusterScope(),
)
# Route to nodes in a specific datacenter
config = AlternatorConfig(
seed_hosts=["node1"],
port=8000,
routing_scope=DatacenterScope(datacenter="us-east-1"),
)
# Route to nodes in a specific rack (with fallback)
config = AlternatorConfig(
seed_hosts=["node1"],
port=8000,
routing_scope=RackScope(datacenter="us-east-1", rack="rack1"),
)
Scopes automatically fall back to broader scopes if no nodes are available:
RackScope→DatacenterScope→ClusterScope
Key Affinity (LWT Optimization)
For Lightweight Transactions (conditional writes), routing requests for the same partition key to the same node can improve performance:
from alternator import (
AlternatorConfigBuilder,
KeyRouteAffinityMode,
)
config = (
AlternatorConfigBuilder()
.with_seeds("node1")
.with_port(8000)
.with_key_affinity(
mode=KeyRouteAffinityMode.RMW, # Only for read-modify-write ops
table_pk_map={"my_table": "pk"}, # Optional: preload PK names
)
.build()
)
Affinity Modes
| Mode | Description |
|---|---|
NONE |
Disabled (default round-robin) |
RMW |
Only for operations with ConditionExpression or ReturnValues |
ANY_WRITE |
For all write operations (PutItem, UpdateItem, DeleteItem) |
TLS Configuration
from alternator import TlsConfig, TlsSessionCacheConfig
from pathlib import Path
# Use system CA certificates (default)
tls = TlsConfig.system_default()
# Use custom CA certificate
tls = TlsConfig.with_custom_ca(Path("/path/to/ca.pem"))
# Trust all certificates (INSECURE - dev only)
tls = TlsConfig.trust_all()
# Full configuration
tls = TlsConfig(
custom_ca_cert_paths=[Path("/path/to/ca.pem")],
trust_system_ca_certs=True,
verify_hostname=True,
session_cache=TlsSessionCacheConfig(
enabled=True,
cache_size=1024,
timeout_seconds=86400,
),
)
Request Compression
Enable gzip compression for large request bodies:
Note: Gzip request compression requires ScyllaDB 2026.1.0 or later. Earlier versions do not support the
Content-Encoding: gzipheader. Response compression (Accept-Encoding: gzip) is not yet supported by Alternator.
from alternator import AlternatorConfigBuilder, CompressionAlgorithm
config = (
AlternatorConfigBuilder()
.with_seeds("node1")
.with_port(8000)
.with_compression(
CompressionAlgorithm.GZIP,
min_size=1024, # Only compress bodies >= 1KB
)
.build()
)
Error Handling
from alternator import (
AlternatorClient,
AlternatorConfig,
AlternatorError,
NoNodesAvailableError,
ConfigurationError,
)
try:
config = AlternatorConfig(seed_hosts=[], port=8000)
except ConfigurationError as e:
print(f"Invalid configuration: {e}")
try:
with AlternatorClient(config) as client:
client.list_tables()
except NoNodesAvailableError as e:
print(f"No nodes available: {e}")
except AlternatorError as e:
print(f"Alternator error: {e}")
Logging
The library uses Python's standard logging module with the logger name alternator:
import logging
# Enable debug logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("alternator").setLevel(logging.DEBUG)
Log levels:
INFO: Node discovery eventsWARNING: Fallback events, connection issuesDEBUG: Detailed routing decisions, node listsERROR: Failed operations
DynamoDB Resource Interface
For table-oriented operations, use AlternatorResource which wraps boto3's DynamoDB resource:
from alternator import AlternatorConfig, AlternatorResource
config = AlternatorConfig(seed_hosts=["192.168.1.1"], port=8000)
with AlternatorResource(config) as resource:
table = resource.Table("my_table")
table.put_item(Item={"pk": "user123", "data": "hello"})
response = table.get_item(Key={"pk": "user123"})
You can also use the factory function:
from alternator import create_resource, close_resource, AlternatorConfig
config = AlternatorConfig(seed_hosts=["node1"], port=8000)
resource = create_resource(config)
try:
table = resource.Table("my_table")
table.scan()
finally:
close_resource(resource)
Manual Resource Management
If you prefer not to use context managers:
from alternator import create_client, close_client, AlternatorConfig
config = AlternatorConfig(seed_hosts=["node1"], port=8000)
client = create_client(config)
try:
client.list_tables()
finally:
close_client(client) # Stop background refresh thread
Async equivalent:
from alternator import AlternatorConfig
from alternator.async_client import create_async_client, close_async_client
config = AlternatorConfig(seed_hosts=["node1"], port=8000)
client = await create_async_client(config)
try:
await client.list_tables()
finally:
await close_async_client(client)
Production Recommendations
- Connection pool sizing: The default
max_pool_connections=200works for most workloads. Increase if you see connection pool exhaustion warnings under high concurrency. - Refresh intervals: Default active refresh (1s) is appropriate for dynamic clusters. For stable clusters, increase
active_refresh_interval_msto reduce discovery overhead. - Timeouts: Default
discovery_timeout_seconds=5.0andread_timeout_seconds=30.0are conservative. Tune based on your network latency and query complexity. - Monitoring: Enable
INFO-level logging for thealternatorlogger to track node discovery events. UseDEBUGfor detailed routing decisions during troubleshooting. - Seed hosts: Configure at least 2-3 seed hosts for redundancy in case one seed is temporarily unavailable during startup.
Thread Safety
Sync clients created by create_client / AlternatorClient are thread-safe: the underlying node selection, round-robin counter, and node list updates are all protected by locks. You can safely share a single client across multiple threads.
Async clients created by create_async_client / AsyncAlternatorClient are safe to use from multiple concurrent coroutines within the same event loop. Do not share an async client across different event loops.
Known Limitations
- Request Compression: Gzip compression requires ScyllaDB 2026.1.0+. Response compression is not yet supported by Alternator.
- TLS Session Cache Settings: The
cache_sizeandtimeout_secondsparameters inTlsSessionCacheConfigare not currently used by Python'ssslmodule. Only theenabledflag controls session ticket behavior. - Async Key Affinity: For async clients, partition key auto-discovery happens asynchronously. The first request for an unknown table will use round-robin routing while discovery runs in the background. Subsequent requests will use affinity. Preloading via
table_pk_mapavoids this initial miss. - Batch Operations: Key affinity routing does not support
BatchWriteItemoperations with items targeting different partition keys.
Development
# Clone the repository
git clone https://github.com/scylladb/alternator-client-python.git
cd alternator-client-python
# Install in development mode
make install
# Run tests
make test-unit
# Run linting
make lint
# Start local Scylla cluster for integration tests
make scylla-start
make test-integration
make scylla-stop
License
Apache License 2.0
Contributing
Contributions are welcome! Please read the contributing guidelines before submitting a pull request.
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 alternator_client-1.0.0.tar.gz.
File metadata
- Download URL: alternator_client-1.0.0.tar.gz
- Upload date:
- Size: 43.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c9da5eaa8cf8b3b9b0c13e369d40195bf0bc01b093295bf8c13e2c351f5eceb9
|
|
| MD5 |
dd529b5ae348ccb3f4e5726174ec19a8
|
|
| BLAKE2b-256 |
9942bec712229e7703c7a672c18772883b72ccf7154c6e06a7bd785505fbc6f4
|
Provenance
The following attestation bundles were made for alternator_client-1.0.0.tar.gz:
Publisher:
release.yml on scylladb/alternator-client-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
alternator_client-1.0.0.tar.gz -
Subject digest:
c9da5eaa8cf8b3b9b0c13e369d40195bf0bc01b093295bf8c13e2c351f5eceb9 - Sigstore transparency entry: 953393112
- Sigstore integration time:
-
Permalink:
scylladb/alternator-client-python@f3d0de5ca3305f46df3309d0eb8a47ffa7d01304 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/scylladb
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f3d0de5ca3305f46df3309d0eb8a47ffa7d01304 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file alternator_client-1.0.0-py3-none-any.whl.
File metadata
- Download URL: alternator_client-1.0.0-py3-none-any.whl
- Upload date:
- Size: 47.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a26645f8e893d325576ebd7dfe78241f1c1fbf6f64c240bb61febfbe846889b
|
|
| MD5 |
7baccffa6e5fb9353f34e474cc4215fd
|
|
| BLAKE2b-256 |
9ad2afac457c15143f24421ed88b065b22bc188e78f319f9de2a507a075ddfee
|
Provenance
The following attestation bundles were made for alternator_client-1.0.0-py3-none-any.whl:
Publisher:
release.yml on scylladb/alternator-client-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
alternator_client-1.0.0-py3-none-any.whl -
Subject digest:
5a26645f8e893d325576ebd7dfe78241f1c1fbf6f64c240bb61febfbe846889b - Sigstore transparency entry: 953393114
- Sigstore integration time:
-
Permalink:
scylladb/alternator-client-python@f3d0de5ca3305f46df3309d0eb8a47ffa7d01304 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/scylladb
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f3d0de5ca3305f46df3309d0eb8a47ffa7d01304 -
Trigger Event:
workflow_dispatch
-
Statement type: