ScyllaDB Rust Driver

repository·main·Indexed 20 days ago

https://github.com/scylladb/scylla-rust-driver

A high-performance, pure Rust asynchronous client driver for ScyllaDB and Apache Cassandra, built on the Tokio runtime. It features token, shard, and tablet-aware routing, type-safe serialization/deserialization, and support for prepared/batch statements. The driver includes configurable load balancing, retry, and speculative execution policies, as well as TLS support via OpenSSL and Rustls, and compression via LZ4 and Snappy.

Tokens
97.9K
Snippets
245
Records
332
Agent score
71%

What's inside scylla-rust-driver

  1. Core features of the ScyllaDB Rust Driver

    main

    The driver provides a comprehensive set of features for high-performance database interaction:

    • Routing: Token-aware, Shard-aware, and Tablet-aware routing (ScyllaDB specific).
    • Data Handling: Type-safe serialization/deserialization, zero-copy deserialization, and derive macros for user structs.
    • Query Execution: Prepared, unprepared, and batch statements; transparent and manual query paging; and CachingSession for transparent statement preparation.
    • Policies: Configurable Load balancing, Retry, and Speculative execution policies.
    • Security & Performance: TLS support (OpenSSL and Rustls), compression (LZ4 and Snappy), and authentication.
    • Observability: Driver-side metrics, query execution history, and CQL tracing.
  2. Configure Retry Policies in the ScyllaDB Rust Driver

    main

    The driver uses a Retry Policy to decide whether to retry a query after a failure. You can configure a retry policy at the Session level (applying to all queries) or for a single query.

    Built-in Retry Policies

    • Fallthrough Retry Policy: Never retries; all errors are returned directly to the user.
    • Default Retry Policy: The default behavior; it retries queries if there is a high probability of success.
    • Downgrading Consistency Retry Policy: Behaves like the Default Retry Policy but also attempts retries using a lower Consistency level in certain failure scenarios.

    Custom Retry Policies

    You can implement your own logic by implementing the RetryPolicy and RetrySession traits.

  3. How load balancing works in the Scylla Rust Driver

    main

    The driver uses a load balancing policy to decide which node(s) and shard(s) to contact for a query. This is represented as a plan: an ordered list of targets (where a target is a <node, optional shard> pair). The first elements in the plan are the preferred targets (e.g., replicas or low-latency nodes).

    Key distinctions:

    • Load Balancing Policy: Determines the order of targets for queries.
    • Host Filter: Determines which nodes connections are opened to. To blacklist specific nodes, use scylla::policies::host_filter::HostFilter via SessionBuilder::host_filter instead of a load balancing policy.
    • Custom Policies: You can implement the LoadBalancingPolicy trait to create custom logic, but the recommended approach is to use the DefaultPolicy with token-awareness enabled and latency-awareness disabled.
  4. How Client Routes work

    main

    In a Client Routes setup, the driver uses a connection ID (assigned by your cloud provider) to map cluster nodes to specific proxy endpoints.

    Workflow:

    1. The driver queries system.client_routes on startup and during metadata refreshes to find the (address, port) pair for each node, filtered by your configured connection IDs.
    2. The driver subscribes to CLIENT_ROUTES_CHANGE events to update routing information dynamically without requiring a full metadata refresh.

    Limitations:

    • No Mixed Clusters: All nodes in the cluster must be reachable through ClientRoutes. You cannot mix nodes reachable via ClientRoutes with nodes reachable directly.
    • No TLS: TLS is not yet supported for Client Routes.
    • Shard Awareness: Advanced shard awareness (targeting specific source ports) is disabled because proxy infrastructure typically does not preserve it. However, basic shard awareness remains functional, ensuring requests are still routed to the correct shards.
  5. How DefaultPolicy resolves node location preferences

    main

    Node location preferences (datacenter and rack) determine how DefaultPolicy prioritizes nodes. Preferences are resolved at two levels:

    1. Session-level preference: Set via SessionBuilder (e.g., SessionBuilder::prefer_datacenter()). This is the fallback used by all policies.
    2. Policy-level preference: Set directly on DefaultPolicyBuilder. A policy-level preference overrides the session-level preference.

    Effective Preference Modes

    • No preference: All nodes are treated as local.
    • Preferred datacenter: Nodes in the preferred DC are "local", others are "remote".
    • Preferred datacenter and rack: Within the preferred DC, nodes in the preferred rack are tried first, then other replicas in the DC, then remote replicas.
  6. Understand the DefaultRetryPolicy behavior

    main

    The DefaultRetryPolicy (implemented in scylla::policies::retry::DefaultRetryPolicy) is the driver's default mechanism for retrying queries when there is a high probability of success on a subsequent attempt. Its behavior is modeled after the DataStax Java Driver.

    Key Rules

    • LWT/Serial Consistency Safety: If the request consistency is a *_SERIAL level (Lightweight Transactions/Paxos), the policy returns DontRetry regardless of the error type or idempotence. This prevents unexpected semantics during the LWT protocol.
    • Per-request Retry Budget: For certain error classes (Unavailable, ReadTimeout, and WriteTimeout), the policy uses a "at most once" guard. A flag is flipped after the first retry for that specific error class within a single request attempt. Subsequent identical errors for that request will result in DontRetry. These flags are reset via RetrySession::reset() between different requests.

    Decision Matrix

    The policy's decision to retry depends on the error type and whether the statement is marked as idempotent.

    ErrorIdempotent statementNon-idempotent statement
    Broken connectionretry on next targetdon't retry
    DbError::Overloaded / ServerError / TruncateErrorretry on next targetdon't retry
    DbError::Unavailableretry on next target (at most once)retry on next target (at most once)
    DbError::ReadTimeout (received >= required, no data)retry on same target (at most once)retry on same target (at most once)
    DbError::ReadTimeout (other shapes)don't retrydon't retry
    DbError::WriteTimeout, WriteType::BatchLogretry on same target (at most once)don't retry
    DbError::WriteTimeout (other write types)don't retrydon't retry
    DbError::IsBootstrappingretry on next targetretry on next target
    RequestAttemptError::UnableToAllocStreamIdretry on next targetretry on next target
    Consistency::is_serial() (LWT)don't retrydon't retry
    Anything else (SyntaxError, Invalid, Unauthorized, etc.)don't retrydon't retry
  7. Handle NULL and Unset values

    main

    When working with nullable columns, you have two primary ways to handle values:

    1. Option<T> (NULL): Sending None will insert a NULL value into the database. Note that in ScyllaDB/Cassandra, inserting a NULL is treated as a delete operation and generates a tombstone, which can impact performance.
    2. Unset (Better Performance): To avoid generating tombstones, use MaybeUnset<T> or the Unset type. This tells the database to leave the column untouched rather than explicitly writing a NULL.

    Use MaybeUnset::Unset or simply Unset for optimal performance during inserts.

    use scylla::value::{MaybeUnset, Unset};
    
    // 1. Sending a NULL (generates a tombstone)
    let null_i32: Option<i32> = None;
    session
        .query_unpaged("INSERT INTO ks.tab (a) VALUES(?)", (null_i32,))
        .await?;
    
    // 2. Sending an Unset value (better performance, no tombstone)
    let unset_i32: MaybeUnset<i32> = MaybeUnset::Unset;
    session
        .query_unpaged("INSERT INTO ks.tab (a) VALUES(?)", (unset_i32,))
        .await?;
    
    // Or simply using Unset directly
    session
        .query_unpaged("INSERT INTO ks.tab (a) VALUES(?)", (Unset,))
        .await?;
  8. Understand the deserialization framework traits

    main

    The scylla-cql-core deserialization framework is built around two primary traits used to convert CQL query results into Rust types:

    1. DeserializeValue<'frame, 'metadata>: Used to deserialize a single CQL value (an individual element of a row).
    2. DeserializeRow<'frame, 'metadata>: Used to deserialize an entire row of a query result.

    Important Distinction: Final vs. Partial Types

    Not all implementors of these traits are 'final' types. Some are type deserializers (partially deserialized types) that facilitate further deserialization. Examples include:

    • ListlikeIterator
    • UdtIterator
    • ColumnIterator

    Final types (like i32 or String) are completely deserialized and can often live independently of the result metadata.

  9. Prepared vs Unprepared statements

    main

    The driver distinguishes between PreparedStatement and Statement (unprepared).

    Prepared Statements

    • Use case: Repeated operations.
    • API: Use execute_* methods.
    • Benefits: High performance (parsed once by DB), advanced load balancing (node/shard awareness), and metadata-based bind value verification.
    • Best Practice: Prepare a statement once (e.g., store in a variable, static, or struct field) and execute it multiple times. Do not prepare the same statement before every execution.

    Unprepared Statements

    • Use case: One-shot operations.
    • API: Use query_* methods.
    • Drawbacks: Poor performance (parsed every time), primitive load balancing, and higher overhead because the driver may silently prepare them internally if they contain bind markers (?).
    // Example pattern for prepared statements
    // 1. Prepare once
    let prepared = session.prepare("SELECT * FROM users WHERE id = ?").await?;
    
    // 2. Execute multiple times
    for id in user_ids {
        session.execute(&prepared, (id,)).await?;
    }
  10. Handle keyspace case sensitivity

    main

    In CQL, keyspace names can be case-insensitive (unquoted) or case-sensitive (quoted). When using Session::use_keyspace, the second argument controls this behavior:

    • false: The name is treated as case-insensitive. For example, MY_KEYSPACE and my_keyspace will both resolve to the same keyspace if it was created without quotes.
    • true: The name is wrapped in double quotes ("), making it case-sensitive.

    If you have keyspaces with the same name but different casing (e.g., my_keyspace and MY_KEYSPACE), you must use the case_sensitive flag correctly to target the intended one.

    // Targets 'my_keyspace' (case-insensitive)
    session.use_keyspace("my_keyspace", false).await?;
    
    // Targets 'my_keyspace' (case-sensitive, wrapped in quotes)
    session.use_keyspace("my_keyspace", true).await?;
    
    // Targets 'my_keyspace' (case-insensitive, even if provided in uppercase)
    session.use_keyspace("MY_KEYSPACE", false).await?;
    
    // Targets 'MY_KEYSPACE' (case-sensitive, wrapped in quotes)
    session.use_keyspace("MY_KEYSPACE", true).await?;
  11. Understand how query execution history is structured

    main

    The driver's execution history is represented by StructuredHistory. This abstraction organizes the execution timeline into attempts and speculative fibers:

    • Non-speculative attempts: These are the initial sequential attempts made by the driver. If an attempt fails, the driver consults its retry policy to decide whether to try again on the same or a different node.
    • Speculative fibers: When speculative execution is enabled and a certain time threshold is passed, the driver spawns a 'speculative fiber'. A fiber is a parallel execution path that performs its own sequence of attempts. Multiple fibers can be active simultaneously if the query has not yet been satisfied.

    By analyzing the StructuredHistory, you can see exactly when each attempt was sent, which node it was sent to, and whether it was part of the original sequential execution or a parallel speculative fiber.