Current section
Files
Jump to
Current section
Files
examples/05_advanced_batch_with_checkpointing.exs
defmodule Examples.AdvancedBatchWithCheckpointing do
@moduledoc """
Advanced batch operations with checkpointing and complex state.
This example demonstrates:
- Using checkpointing to resume from failures
- Complex state management (maps, structs)
- Multiple health checks
- Error handling and retry logic
"""
use AdaptiveBackfill
defmodule MigrationState do
@moduledoc "Complex state for tracking migration progress"
defstruct offset: 0,
processed_count: 0,
failed_ids: [],
last_processed_id: nil,
started_at: nil
end
# Complex batch operation with state tracking
batch_operation :migrate_with_tracking,
initial_state: %MigrationState{started_at: DateTime.utc_now()} do
mode :sync
health_checks [
&check_database_connections/0,
&check_replication_lag/0,
&check_disk_space/0
]
# Optional: Add checkpointing
# checkpoint adapter: MyCheckpointAdapter, name: :user_migration
handle_batch fn state ->
IO.puts("\n--- Batch #{div(state.offset, 100) + 1} ---")
IO.puts("Progress: #{state.processed_count} processed, #{length(state.failed_ids)} failed")
# Fetch users
users = fetch_users_batch(state.offset, 100)
if Enum.empty?(users) do
print_final_stats(state)
:done
else
# Process batch and collect results
{successful, failed} = process_batch_with_error_handling(users)
# Update state
new_state = %{
state
| offset: state.offset + 100,
processed_count: state.processed_count + length(successful),
failed_ids: state.failed_ids ++ failed,
last_processed_id: List.last(users).id
}
{:ok, new_state}
end
end
on_complete fn result ->
case result do
:done ->
IO.puts("\n✓ Migration completed!")
{:halt, reason} ->
IO.puts("\n✗ Migration halted: #{inspect(reason)}")
IO.puts("You can resume from the last checkpoint")
end
end
end
# Batch operation with retry logic
batch_operation :sync_with_retry, initial_state: {0, %{retries: %{}}} do
mode :async
health_checks [&check_external_api/0]
handle_batch fn {offset, metadata} ->
IO.puts("\n--- Syncing batch at offset #{offset} ---")
records = fetch_records_to_sync(offset, 50)
if Enum.empty?(records) do
print_retry_summary(metadata.retries)
:done
else
# Sync with retry logic
{synced, failed} = sync_records_with_retry(records, metadata.retries)
# Update retry metadata
new_retries =
Enum.reduce(failed, metadata.retries, fn record_id, acc ->
retry_count = Map.get(acc, record_id, 0) + 1
if retry_count < 3 do
Map.put(acc, record_id, retry_count)
else
IO.puts(" ⚠ Record #{record_id} failed after 3 retries, skipping")
Map.delete(acc, record_id)
end
end)
new_metadata = %{metadata | retries: new_retries}
{:ok, {offset + 50, new_metadata}}
end
end
on_complete fn result ->
case result do
:done -> IO.puts("\n✓ Sync completed!")
{:halt, reason} -> IO.puts("\n✗ Sync halted: #{inspect(reason)}")
end
end
end
# Batch operation with dynamic batch sizing
batch_operation :adaptive_batch_size, initial_state: {0, 100} do
mode :sync
health_checks [&check_system_load/0]
handle_batch fn {offset, batch_size} ->
IO.puts("\n--- Processing with batch size #{batch_size} at offset #{offset} ---")
records = fetch_records(offset, batch_size)
if Enum.empty?(records) do
:done
else
# Measure processing time
{time_us, _result} = :timer.tc(fn -> process_records(records) end)
time_ms = div(time_us, 1000)
# Adjust batch size based on processing time
new_batch_size = adjust_batch_size(batch_size, time_ms)
IO.puts(" Processing took #{time_ms}ms, adjusting batch size to #{new_batch_size}")
{:ok, {offset + batch_size, new_batch_size}}
end
end
on_complete fn result ->
case result do
:done -> IO.puts("\n✓ Adaptive processing completed!")
{:halt, reason} -> IO.puts("\n✗ Processing halted: #{inspect(reason)}")
end
end
end
# Helper functions
defp fetch_users_batch(offset, limit) do
if offset >= 1000 do
[]
else
Enum.map(1..limit, fn i ->
%{id: offset + i, email: "user#{offset + i}@example.com", data: "data"}
end)
end
end
defp process_batch_with_error_handling(users) do
Enum.reduce(users, {[], []}, fn user, {successful, failed} ->
# Simulate processing with occasional failures
if :rand.uniform(100) > 5 do
# Success
:timer.sleep(10)
{[user.id | successful], failed}
else
# Failure
IO.puts(" ✗ Failed to process user #{user.id}")
{successful, [user.id | failed]}
end
end)
end
defp fetch_records_to_sync(offset, limit) do
if offset >= 500 do
[]
else
Enum.map(1..limit, fn i -> %{id: offset + i} end)
end
end
defp sync_records_with_retry(records, retry_map) do
Enum.reduce(records, {[], []}, fn record, {synced, failed} ->
retry_count = Map.get(retry_map, record.id, 0)
# Higher success rate on retries
success_rate = 90 + retry_count * 5
if :rand.uniform(100) <= success_rate do
:timer.sleep(20)
{[record.id | synced], failed}
else
IO.puts(" ✗ Failed to sync record #{record.id} (attempt #{retry_count + 1})")
{synced, [record.id | failed]}
end
end)
end
defp fetch_records(offset, limit) do
if offset >= 800 do
[]
else
Enum.map(1..limit, fn i -> %{id: offset + i} end)
end
end
defp process_records(records) do
# Simulate variable processing time
base_time = length(records) * 10
jitter = :rand.uniform(50)
:timer.sleep(base_time + jitter)
end
defp adjust_batch_size(current_size, processing_time_ms) do
cond do
# Too fast, increase batch size
processing_time_ms < 500 and current_size < 500 ->
min(current_size + 50, 500)
# Too slow, decrease batch size
processing_time_ms > 2000 and current_size > 50 ->
max(current_size - 50, 50)
# Just right
true ->
current_size
end
end
defp print_final_stats(state) do
duration = DateTime.diff(DateTime.utc_now(), state.started_at)
IO.puts("\n=== Migration Statistics ===")
IO.puts("Total processed: #{state.processed_count}")
IO.puts("Failed: #{length(state.failed_ids)}")
IO.puts("Duration: #{duration} seconds")
if length(state.failed_ids) > 0 do
IO.puts("Failed IDs: #{inspect(Enum.take(state.failed_ids, 10))}...")
end
end
defp print_retry_summary(retries) do
if map_size(retries) > 0 do
IO.puts("\n⚠ Records still pending retry: #{map_size(retries)}")
end
end
# Health check functions
defp check_database_connections do
connections = :rand.uniform(100)
if connections > 80 do
{:halt, "Database connection pool exhausted: #{connections}/100"}
else
IO.puts(" ✓ DB connections: #{connections}/100")
:ok
end
end
defp check_replication_lag do
lag_seconds = :rand.uniform(60)
if lag_seconds > 30 do
{:halt, "Replication lag too high: #{lag_seconds}s"}
else
IO.puts(" ✓ Replication lag: #{lag_seconds}s")
:ok
end
end
defp check_disk_space do
free_gb = :rand.uniform(100)
if free_gb < 10 do
{:halt, "Low disk space: #{free_gb}GB free"}
else
IO.puts(" ✓ Disk space: #{free_gb}GB free")
:ok
end
end
defp check_external_api do
latency_ms = :rand.uniform(500)
if latency_ms > 400 do
{:halt, "API latency too high: #{latency_ms}ms"}
else
IO.puts(" ✓ API latency: #{latency_ms}ms")
:ok
end
end
defp check_system_load do
load = :rand.uniform(100) / 100
if load > 0.9 do
{:halt, "System load too high: #{Float.round(load, 2)}"}
else
IO.puts(" ✓ System load: #{Float.round(load, 2)}")
:ok
end
end
end
# Run the examples
# Examples.AdvancedBatchWithCheckpointing.migrate_with_tracking()
# Examples.AdvancedBatchWithCheckpointing.sync_with_retry()
# Examples.AdvancedBatchWithCheckpointing.adaptive_batch_size()