Error Handling Guide

Copy Markdown View Source

Comprehensive error handling for AshScylla


Overview

AshScylla provides structured error handling for ScyllaDB-specific errors. It categorizes Xandra errors into meaningful types and provides actionable suggestions for developers.


Error Architecture


          AshScylla.DataLayer                        
  (wraps Xandra errors with wrap_xandra_error/1)    

                   
                   

          AshScylla.Error                            
   wrap_xandra_error/1  - Convert errors           
   format_error/1      - Format for display        
   retryable?/1        - Check if retryable        

                   
                   

    AshScylla.Error.ScyllaError                      
   Categorizes errors by type                       
   Provides user-friendly suggestions               
   Structured error information                     

Error Types

Error TypeDescriptionWhen It Occurs
:syntax_errorInvalid CQL syntaxMalformed CQL queries
:query_errorGeneral query execution errorInvalid queries
:schema_errorSchema-related errorTable/keyspace/column not found
:overloadedScyllaDB node overloadedHigh load on cluster
:timeoutQuery timeoutRead/write timeout
:consistency_errorConsistency level not metInsufficient replicas
:unauthorizedPermission deniedInvalid credentials/permissions
:already_existsResource conflictTable/keyspace already exists
:not_foundResource missingTable/keyspace doesn't exist
:connection_timeoutConnection timeoutNetwork issues
:connection_closedConnection closedNode unavailable
:connection_errorGeneral connection errorNetwork/configuration issues

Using Error Handling

Basic Error Handling

case AshScylla.DataLayer.run_query(query, resource) do
  {:ok, results} ->
    {:ok, results}

  {:error, %AshScylla.Error.ScyllaError{} = error} ->
    # Log the detailed error
    Logger.error("Database error: #{AshScylla.Error.format_error(error)}")

    # Check if we should retry
      if AshScylla.Error.retryable?(error) do
        {:retry, error}
      else
        {:error, error}
      end

  {:error, error} ->
    # Handle other errors
    {:error, error}
end

Error Struct Fields

%AshScylla.Error.ScyllaError{
  type: :overloaded,           # Error category (atom)
  reason: "...",               # Original error reason
  message: "...",              # Human-readable message
  suggestion: "...",           # Actionable suggestion
  original_error: %Xandra...   # Original Xandra error
}

Retry Logic

Checking Retryability

error = %AshScylla.Error.ScyllaError{type: :overloaded}

if AshScylla.Error.retryable?(error) do
  IO.puts("This error is retryable")
else
  IO.puts("This error is NOT retryable")
end

Retryable Errors

Error TypeRetryable?
:overloaded✅ Yes
:connection_timeout✅ Yes
:timeout✅ Yes
:connection_closed✅ Yes
:connection_error✅ Yes
:syntax_error❌ No
:schema_error❌ No
:unauthorized❌ No
:already_exists❌ No
:not_found❌ No

Implementing Retry with Backoff

defmodule MyApp.Database do
  @max_retries 3
  @base_delays %{
    overloaded: 1000,
    timeout: 500,
    connection_timeout: 2000,
    connection_closed: 1000,
    connection_error: 2000
  }

  def execute_with_retry(operation, retries \\ 0) do
    case operation.() do
      {:ok, result} ->
        {:ok, result}

      {:error, %AshScylla.Error.ScyllaError{} = error} ->
        if AshScylla.Error.retryable?(error) and retries < @max_retries do
          delay = Map.get(@base_delays, error.type, 500)
          sleep_time = delay * :math.pow(2, retries)
          Process.sleep(round(sleep_time))
          execute_with_retry(operation, retries + 1)
        else
          {:error, error}
        end

      {:error, error} ->
        {:error, error}
    end
  end
end

Error Formatting

Formatting Errors for Display

error = %AshScylla.Error.ScyllaError{
  type: :overloaded,
  message: "ScyllaDB node is overloaded",
  suggestion: "Increase timeout, reduce load, or scale your cluster"
}

formatted = AshScylla.Error.format_error(error)
IO.puts(formatted)

# Output:
# ScyllaDB Error (overloaded): ScyllaDB node is overloaded
# Suggestion: Increase timeout, reduce load, or scale your cluster

Common Error Scenarios

1. Connection Refused

%AshScylla.Error.ScyllaError{
  type: :connection_error,
  message: "Connection refused",
  suggestion: "Check if ScyllaDB is running and accessible at the configured address"
}

Solution:

  • Verify ScyllaDB is running: podman ps
  • Check nodes configuration in config/config.exs
  • Ensure firewall allows connections

2. Syntax Error

%AshScylla.Error.ScyllaError{
  type: :syntax_error,
  message: "Invalid CQL syntax at line 1: SELECT * FROM",
  suggestion: "Check CQL syntax, ensure proper commas, parentheses, and keywords"
}

Solution:

  • Review CQL query syntax
  • Check for missing commas, parentheses
  • Validate table/column names

3. Schema Error (Table Not Found)

%AshScylla.Error.ScyllaError{
  type: :schema_error,
  message: "Table 'users' not found",
  suggestion: "Run migrations to create the table, or verify the table name and keyspace"
}

Solution:

4. Overloaded Node

%AshScylla.Error.ScyllaError{
  type: :overloaded,
  message: "ScyllaDB node is overloaded",
  suggestion: "Increase timeout, reduce load, or scale your cluster"
}

Solution:

  • Increase request_timeout in repo config
  • Scale ScyllaDB cluster (add nodes)
  • Optimize queries
  • Use retry logic with backoff

5. Timeout

%AshScylla.Error.ScyllaError{
  type: :timeout,
  message: "Query timed out after 120000ms",
  suggestion: "Increase request_timeout in repo config, optimize query, or use pagination"
}

Solution:

  • Increase request_timeout: config :my_app, MyApp.Repo, request_timeout: 300_000
  • Optimize slow queries
  • Use pagination for large result sets

6. Record Not Found

When fetch_by_primary_key returns an empty result set (no matching record), AshScylla returns a structured ScyllaError instead of crashing:

{:error, %AshScylla.Error.ScyllaError{
  type: :query_error,
  message: "Record not found in table users with primary key %{id: \"uuid\"}"
}}

This prevents MatchError crashes when a record is deleted between operations.

7. Aggregate Query Empty Results

When run_aggregate_query returns an empty result set from ScyllaDB, it gracefully returns 0 for the count instead of crashing:

# Empty result from COUNT query returns 0
{:ok, %{total: 0}} = DataLayer.run_aggregate_query(query, [%{kind: :count, name: :total}], resource)

8. Non-Indexed Column Filter

%AshScylla.Error.ScyllaError{
  type: :schema_error,
  message: "Cannot execute this query as it might involve data filtering... use ALLOW FILTERING"
}

AshScylla rejects such queries at query-plan time when a query filters on a column that is neither part of the primary key nor has a secondary index.

Solution A (Recommended): Add a secondary index

scylla do
  secondary_index :game_id
end

Solution B: Use a materialized view for the query pattern.

9. Consistency Level Not Met

%AshScylla.Error.ScyllaError{
  type: :consistency_error,
  message: "Consistency level QUORUM not met",
  suggestion: "Lower consistency level, check replica availability, or increase replication factor"
}

Solution:

  • Lower consistency level: scylla do consistency :one end
  • Check if replicas are available
  • Verify replication factor in keyspace

10. CQL Injection Prevention

AshScylla sanitizes all CQL identifiers through AshScylla.Identifier:

AshScylla.Identifier.sanitize!("users; DROP TABLE users")
# => ** (ArgumentError) Invalid CQL identifier: "users; DROP TABLE users"

All table names, column names, keyspace names, and index names are validated before interpolation into CQL strings.


Testing Error Handling

AshScylla includes comprehensive tests for error handling:

# Run error handling tests
mix test test/unit/error/

Test Coverage

  • All error types and categorization
  • Retryability classification
  • Error formatting
  • Connection error subclassification
  • Filter validation (unindexed column rejection)
  • CQL injection prevention (identifier sanitization)

Best Practices

1. Always Handle Errors

# Bad: Not handling errors
Ash.create(resource)

# Good: Proper error handling
case Ash.create(resource) do
  {:ok, result} -> result
  {:error, error} -> handle_error(error)
end

2. Use Retry for Transient Errors

# Retry connection and timeout errors
if AshScylla.Error.retryable?(error) do
  # Implement your own delay logic based on error.type
  Process.sleep(1000)
  # retry
end

3. Log Errors with Context

Logger.error("""
Database error occurred:
- Error: #{AshScylla.Error.format_error(error)}
- Resource: #{inspect(resource)}
- Operation: #{operation}
""")

4. Provide User-Friendly Messages

def handle_database_error(%AshScylla.Error.ScyllaError{} = error) do
  %{message: message, suggestion: suggestion} = error

  """
  A database error occurred: #{message}

  What you can do: #{suggestion}
  """
end

Module Documentation

For detailed API documentation, see:

# Get help in IEx
h AshScylla.Error
h AshScylla.Error.ScyllaError

License

Apache License 2.0 - see LICENSE file for details.