AdaptiveBackfill

TestsHex.pmDocumentation

Adaptive backfill library with health checks for Elixir. Supports both single operations and batch processing with synchronous and asynchronous health monitoring.

Features

Installation

Add adaptive_backfill to your list of dependencies in mix.exs:

def deps do
  [
    {:adaptive_backfill, "~> 0.1.0"}
  ]
end

Usage

DSL API (Recommended)

Define backfills using the clean DSL syntax:

defmodule MyApp.Backfills do
  use AdaptiveBackfill

  # Single operation backfill
  single_operation :process_user do
    mode :sync
    health_checks [&check_database/0, &check_memory/0]
    
    handle fn health_check ->
      # Do some work
      user = get_next_user()
      process_user(user)
      
      # Check health before continuing
      case health_check.() do
        :ok -> :done
        {:halt, reason} -> {:halt, reason}
      end
    end
    
    on_complete fn result ->
      Logger.info("User processing completed: #{inspect(result)}")
    end
  end

  # Batch operation backfill
  batch_operation :migrate_users, initial_state: 0 do
    mode :async
    health_checks [&DefaultPgHealthCheckers.long_waiting_queries/1]
    
    handle_batch fn offset ->
      users = get_users_batch(offset, 100)
      
      if Enum.empty?(users) do
        :done
      else
        migrate_batch(users)
        {:ok, offset + 100}
      end
    end
    
    on_complete fn result ->
      Logger.info("Migration completed: #{inspect(result)}")
    end
  end
  
  defp check_database, do: :ok
  defp check_memory, do: :ok
end

# Run your backfills
MyApp.Backfills.process_user()
MyApp.Backfills.migrate_users()

# Override options at runtime
MyApp.Backfills.migrate_users(initial_state: 500, mode: :sync)

Non-DSL API

You can also use the struct-based API:

# Single operation
handle = fn health_check ->
  case health_check.() do
    :ok -> :done
    {:halt, reason} -> {:halt, reason}
  end
end

{:ok, opts} = SingleOperationOptions.new(handle, nil, :sync, [&check/0])
AdaptiveBackfill.run(opts)

# Batch operation
handle_batch = fn state ->
  if state < 100, do: {:ok, state + 1}, else: :done
end

{:ok, opts} = BatchOperationOptions.new(0, handle_batch, nil, :async, [&check/0])
AdaptiveBackfill.run(opts)

PostgreSQL Health Checks

# Use built-in PostgreSQL health checkers
health_checkers = DefaultPgHealthCheckers.pg_health_checks(MyApp.Repo)

# Or individual checks
health_checkers = [
  &DefaultPgHealthCheckers.long_waiting_queries/1,
  &DefaultPgHealthCheckers.hot_io_tables/1,
  &DefaultPgHealthCheckers.temp_file_usage/1
]

Modes

Sync Mode

Health checks are executed synchronously on-demand when called.

Async Mode

Health checks run in the background via GenServer and provide cached results.

Development

Running Tests

# Start PostgreSQL
docker-compose up -d

# Run tests
mix test

# Stop PostgreSQL
docker-compose down

Code Quality

# Format code
mix format

# Run linter
mix credo

Examples

Check out the examples/ directory including:

See examples/README.md for detailed documentation.

Documentation

Documentation is available at https://hexdocs.pm/adaptive_backfill.

TODO / Future Improvements

Enhanced Health Check Evaluation

The current health monitoring system uses a simple "fail-fast" approach: if any health check returns {:halt, reason}, the operation halts immediately. Production systems may need different evaluation strategies.

Current Behavior:

# If ANY health check fails, the entire operation halts
health_checks [
  &check_database/0,      # Returns {:halt, :high_load}
  &check_memory/0,        # Never evaluated
  &check_disk_space/0     # Never evaluated
]

Planned Improvements:

  1. Weighted Health Checks - Assign importance levels to different monitors

    health_checks [
      {&check_database/0, weight: :critical},      # Must pass
      {&check_memory/0, weight: :warning},         # Can fail
      {&check_disk_space/0, weight: :info}         # Informational only
    ]
  2. Threshold-Based Evaluation - Halt only when a certain number/percentage of checks fail

    health_check_strategy: {:threshold, 0.5}  # Halt if >50% fail
  3. Custom Evaluation Functions - Let users define their own logic

    health_check_evaluator: fn results ->
      critical_failed = count_failed(results, :critical)
      if critical_failed > 0, do: {:halt, :critical_failure}, else: :ok
    end
  4. Graceful Degradation - Continue with reduced functionality instead of halting

    on_health_warning: fn warnings ->
      # Reduce batch size, increase delays, etc.
      {:continue, adjusted_options}
    end

License

MIT License - see LICENSE for details.