Skip to content
 
 

Repository files navigation

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 /localnodes endpoint
  • 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 remains alternator.

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:

  • RackScopeDatacenterScopeClusterScope

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: gzip header. 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 events
  • WARNING: Fallback events, connection issues
  • DEBUG: Detailed routing decisions, node lists
  • ERROR: 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=200 works 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_ms to reduce discovery overhead.
  • Timeouts: Default discovery_timeout_seconds=5.0 and read_timeout_seconds=30.0 are conservative. Tune based on your network latency and query complexity.
  • Monitoring: Enable INFO-level logging for the alternator logger to track node discovery events. Use DEBUG for 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_size and timeout_seconds parameters in TlsSessionCacheConfig are not currently used by Python's ssl module. Only the enabled flag 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_map avoids this initial miss.
  • Batch Operations: Key affinity routing does not support BatchWriteItem operations 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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages