Skip to main content

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 /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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

alternator_client-1.0.0.tar.gz (43.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

alternator_client-1.0.0-py3-none-any.whl (47.5 kB view details)

Uploaded Python 3

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

Hashes for alternator_client-1.0.0.tar.gz
Algorithm Hash digest
SHA256 c9da5eaa8cf8b3b9b0c13e369d40195bf0bc01b093295bf8c13e2c351f5eceb9
MD5 dd529b5ae348ccb3f4e5726174ec19a8
BLAKE2b-256 9942bec712229e7703c7a672c18772883b72ccf7154c6e06a7bd785505fbc6f4

See more details on using hashes here.

Provenance

The following attestation bundles were made for alternator_client-1.0.0.tar.gz:

Publisher: release.yml on scylladb/alternator-client-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file alternator_client-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for alternator_client-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5a26645f8e893d325576ebd7dfe78241f1c1fbf6f64c240bb61febfbe846889b
MD5 7baccffa6e5fb9353f34e474cc4215fd
BLAKE2b-256 9ad2afac457c15143f24421ed88b065b22bc188e78f319f9de2a507a075ddfee

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page