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        
   retry_delay/1       - Get retry delay           

                   
                   

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

Error Types

Available Error Categories

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
      delay = AshScylla.Error.retry_delay(error)
      {:retry, delay}
    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?Delay (ms)
:overloaded✅ Yes1000
:connection_timeout✅ Yes2000
:timeout✅ Yes500
:connection_closed✅ Yes1000
:connection_error✅ Yes1000
: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
  
  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 = AshScylla.Error.retry_delay(error)
          # Add exponential backoff
          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

# Usage
result = MyApp.Database.execute_with_retry(fn ->
  AshScylla.DataLayer.run_query(query, resource)
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: docker 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:

  • Run migrations: mix ecto.migrate
  • Verify table name in resource configuration
  • Check keyspace configuration

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. 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: ash_scylla do consistency :one end
  • Check if replicas are available
  • Verify replication factor in keyspace

Testing Error Handling

AshScylla includes comprehensive tests for error handling in test/ash_scylla/error_test.exs:

# Run error handling tests
mix test test/ash_scylla/error_test.exs

Test Coverage

  • ✅ 14 tests covering all error types
  • ✅ Tests for retry logic
  • ✅ Tests for error formatting
  • ✅ Tests for error categorization

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
  delay = AshScylla.Error.retry_delay(error)
  Process.sleep(delay)
  # 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.