cleanup worker tweaks
This commit is contained in:
parent
9b76a2ca79
commit
389ce8edeb
4 changed files with 224 additions and 23 deletions
|
|
@ -38,7 +38,7 @@ config :aprsme, Oban,
|
||||||
{Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 7},
|
{Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 7},
|
||||||
{Oban.Plugins.Cron,
|
{Oban.Plugins.Cron,
|
||||||
crontab: [
|
crontab: [
|
||||||
{"0 0 * * *", Aprsme.Workers.PacketCleanupWorker}
|
{"0 */6 * * *", Aprsme.Workers.PacketCleanupWorker}
|
||||||
]}
|
]}
|
||||||
],
|
],
|
||||||
queues: [default: 10, maintenance: 2]
|
queues: [default: 10, maintenance: 2]
|
||||||
|
|
|
||||||
|
|
@ -6,23 +6,32 @@ defmodule Aprsme.Workers.PacketCleanupWorker do
|
||||||
1. Removing packets older than the retention period (1 year by default)
|
1. Removing packets older than the retention period (1 year by default)
|
||||||
2. Providing granular cleanup with custom age parameters
|
2. Providing granular cleanup with custom age parameters
|
||||||
3. Logging statistics about cleanup operations
|
3. Logging statistics about cleanup operations
|
||||||
|
4. Supporting batch processing for large datasets
|
||||||
|
|
||||||
This worker is scheduled to run daily at midnight UTC via Oban's cron feature.
|
This worker is scheduled to run every 6 hours via Oban's cron feature for more frequent,
|
||||||
|
smaller cleanup operations to prevent large deletion spikes.
|
||||||
|
|
||||||
## Functions
|
## Functions
|
||||||
- `perform/1` - Standard cleanup using configured retention period
|
- `perform/1` - Standard cleanup using configured retention period
|
||||||
- `cleanup_packets_older_than/1` - Cleanup packets older than specified days
|
- `cleanup_packets_older_than/1` - Cleanup packets older than specified days
|
||||||
|
- `cleanup_packets_in_batches/1` - Batch cleanup for large datasets
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Oban.Worker, queue: :maintenance, max_attempts: 3
|
use Oban.Worker, queue: :maintenance, max_attempts: 3
|
||||||
|
|
||||||
# Import modules needed for database operations
|
# Import modules needed for database operations
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
alias Aprsme.Packet
|
alias Aprsme.Packet
|
||||||
alias Aprsme.Repo
|
alias Aprsme.Repo
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
# Batch size for cleanup operations to prevent long-running transactions
|
||||||
|
@batch_size 10_000
|
||||||
|
# Maximum time to spend on cleanup in milliseconds (5 minutes)
|
||||||
|
@max_cleanup_time 5 * 60 * 1000
|
||||||
|
|
||||||
@impl Oban.Worker
|
@impl Oban.Worker
|
||||||
@spec perform(Oban.Job.t()) :: :ok | {:error, String.t()}
|
@spec perform(Oban.Job.t()) :: :ok | {:error, String.t()}
|
||||||
def perform(%Oban.Job{args: %{"cleanup_days" => days}}) when is_integer(days) do
|
def perform(%Oban.Job{args: %{"cleanup_days" => days}}) when is_integer(days) do
|
||||||
|
|
@ -31,8 +40,8 @@ defmodule Aprsme.Workers.PacketCleanupWorker do
|
||||||
# Count packets before cleanup for statistics
|
# Count packets before cleanup for statistics
|
||||||
_total_before = count_total_packets()
|
_total_before = count_total_packets()
|
||||||
|
|
||||||
# Perform the cleanup of old packets with custom age
|
# Perform the cleanup of old packets with custom age using batch processing
|
||||||
deleted_count = packets_module().clean_packets_older_than(days)
|
{deleted_count, _} = cleanup_packets_older_than_batched(days)
|
||||||
|
|
||||||
# Log results
|
# Log results
|
||||||
Logger.info("APRS packet cleanup complete: removed #{deleted_count} packets older than #{days} days")
|
Logger.info("APRS packet cleanup complete: removed #{deleted_count} packets older than #{days} days")
|
||||||
|
|
@ -48,8 +57,8 @@ defmodule Aprsme.Workers.PacketCleanupWorker do
|
||||||
# Count packets before cleanup for statistics
|
# Count packets before cleanup for statistics
|
||||||
_total_before = count_total_packets()
|
_total_before = count_total_packets()
|
||||||
|
|
||||||
# Perform the cleanup of old packets (older than 1 year by default)
|
# Perform the cleanup of old packets (older than 1 year by default) using batch processing
|
||||||
deleted_count = cleanup_old_packets()
|
{deleted_count, _} = cleanup_old_packets_batched()
|
||||||
|
|
||||||
# Log results
|
# Log results
|
||||||
retention_days = Application.get_env(:aprsme, :packet_retention_days, 365)
|
retention_days = Application.get_env(:aprsme, :packet_retention_days, 365)
|
||||||
|
|
@ -64,33 +73,134 @@ defmodule Aprsme.Workers.PacketCleanupWorker do
|
||||||
Repo.aggregate(Packet, :count, :id)
|
Repo.aggregate(Packet, :count, :id)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp cleanup_old_packets do
|
defp cleanup_old_packets_batched do
|
||||||
# Use the existing function from Packets context
|
retention_days = Application.get_env(:aprsme, :packet_retention_days, 365)
|
||||||
packets_module().clean_old_packets()
|
cleanup_packets_older_than_batched(retention_days)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Perform cleanup of packets older than a specific number of days.
|
Perform cleanup of packets older than a specific number of days using batch processing.
|
||||||
|
|
||||||
This function provides more granular control over packet cleanup operations.
|
This function provides more granular control over packet cleanup operations and
|
||||||
|
processes deletions in smaller batches to prevent long-running transactions.
|
||||||
|
|
||||||
## Parameters
|
## Parameters
|
||||||
- `days` - Number of days to retain packets (packets older than this will be deleted)
|
- `days` - Number of days to retain packets (packets older than this will be deleted)
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
- {deleted_count, batches_processed}
|
||||||
|
"""
|
||||||
|
@spec cleanup_packets_older_than_batched(pos_integer()) :: {non_neg_integer(), non_neg_integer()}
|
||||||
|
def cleanup_packets_older_than_batched(days) when is_integer(days) and days > 0 do
|
||||||
|
Logger.info("Starting batched APRS packet cleanup for packets older than #{days} days")
|
||||||
|
|
||||||
|
cutoff_time = DateTime.add(DateTime.utc_now(), -days * 86_400, :second)
|
||||||
|
start_time = System.monotonic_time(:millisecond)
|
||||||
|
|
||||||
|
{deleted_count, batches_processed} = cleanup_packets_in_batches(cutoff_time, start_time)
|
||||||
|
|
||||||
|
duration = System.monotonic_time(:millisecond) - start_time
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
"APRS packet cleanup complete: removed #{deleted_count} packets in #{batches_processed} batches over #{duration}ms"
|
||||||
|
)
|
||||||
|
|
||||||
|
{deleted_count, batches_processed}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Clean packets older than a specific number of days.
|
||||||
|
|
||||||
|
This function allows for more granular cleanup operations by specifying
|
||||||
|
the exact age threshold for packet deletion.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
- `days` - Number of days to keep (packets older than this will be deleted)
|
||||||
|
|
||||||
## Returns
|
## Returns
|
||||||
- Number of packets deleted
|
- Number of packets deleted
|
||||||
"""
|
"""
|
||||||
@spec cleanup_packets_older_than(pos_integer()) :: non_neg_integer()
|
@spec cleanup_packets_older_than(pos_integer()) :: non_neg_integer()
|
||||||
def cleanup_packets_older_than(days) when is_integer(days) and days > 0 do
|
def cleanup_packets_older_than(days) when is_integer(days) and days > 0 do
|
||||||
Logger.info("Starting APRS packet cleanup for packets older than #{days} days")
|
{deleted_count, _} = cleanup_packets_older_than_batched(days)
|
||||||
|
|
||||||
deleted_count = packets_module().clean_packets_older_than(days)
|
|
||||||
|
|
||||||
Logger.info("APRS packet cleanup complete: removed #{deleted_count} packets older than #{days} days")
|
|
||||||
|
|
||||||
deleted_count
|
deleted_count
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Clean packets in batches to prevent long-running transactions.
|
||||||
|
|
||||||
|
This function processes deletions in smaller batches and respects time limits
|
||||||
|
to ensure the cleanup doesn't impact system performance.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
- `cutoff_time` - DateTime before which packets should be deleted
|
||||||
|
- `start_time` - Monotonic time when cleanup started (for duration tracking)
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
- {total_deleted_count, batches_processed}
|
||||||
|
"""
|
||||||
|
@spec cleanup_packets_in_batches(DateTime.t(), integer()) :: {non_neg_integer(), non_neg_integer()}
|
||||||
|
def cleanup_packets_in_batches(cutoff_time, start_time) do
|
||||||
|
cleanup_packets_in_batches(cutoff_time, start_time, 0, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp cleanup_packets_in_batches(cutoff_time, start_time, total_deleted, batches_processed) do
|
||||||
|
# Check if we've exceeded the maximum cleanup time
|
||||||
|
current_time = System.monotonic_time(:millisecond)
|
||||||
|
|
||||||
|
if current_time - start_time > @max_cleanup_time do
|
||||||
|
Logger.info("Cleanup time limit reached (#{@max_cleanup_time}ms), stopping batch processing")
|
||||||
|
{total_deleted, batches_processed}
|
||||||
|
else
|
||||||
|
# Get batch of packet IDs to delete
|
||||||
|
packet_ids = get_packet_ids_for_deletion(cutoff_time, @batch_size)
|
||||||
|
|
||||||
|
case packet_ids do
|
||||||
|
[] ->
|
||||||
|
# No more packets to delete
|
||||||
|
{total_deleted, batches_processed}
|
||||||
|
|
||||||
|
ids ->
|
||||||
|
# Delete the batch
|
||||||
|
{deleted_count, _} = Repo.delete_all(from(p in Packet, where: p.id in ^ids))
|
||||||
|
|
||||||
|
new_total_deleted = total_deleted + deleted_count
|
||||||
|
new_batches_processed = batches_processed + 1
|
||||||
|
|
||||||
|
Logger.debug(
|
||||||
|
"Cleanup batch #{new_batches_processed}: deleted #{deleted_count} packets (total: #{new_total_deleted})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Continue with next batch
|
||||||
|
cleanup_packets_in_batches(cutoff_time, start_time, new_total_deleted, new_batches_processed)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Get packet IDs for deletion in batches.
|
||||||
|
|
||||||
|
This function queries for packet IDs that are older than the cutoff time,
|
||||||
|
limiting the result to the specified batch size.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
- `cutoff_time` - DateTime before which packets should be deleted
|
||||||
|
- `batch_size` - Maximum number of IDs to return
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
- List of packet IDs to delete
|
||||||
|
"""
|
||||||
|
@spec get_packet_ids_for_deletion(DateTime.t(), pos_integer()) :: [integer()]
|
||||||
|
def get_packet_ids_for_deletion(cutoff_time, batch_size) do
|
||||||
|
Repo.all(
|
||||||
|
from(p in Packet,
|
||||||
|
where: p.received_at < ^cutoff_time,
|
||||||
|
select: p.id,
|
||||||
|
limit: ^batch_size
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
defp packets_module do
|
defp packets_module do
|
||||||
Application.get_env(:aprsme, :packets_module, Aprsme.Packets)
|
Application.get_env(:aprsme, :packets_module, Aprsme.Packets)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
defmodule Aprsme.Repo.Migrations.EnsurePacketCleanupIndexes do
|
||||||
|
use Ecto.Migration
|
||||||
|
@disable_ddl_transaction true
|
||||||
|
@disable_migration_lock true
|
||||||
|
|
||||||
|
def up do
|
||||||
|
# Ensure index on received_at for efficient time-based deletes
|
||||||
|
execute """
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS packets_received_at_idx
|
||||||
|
ON packets (received_at)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Ensure index on id for efficient batch deletes
|
||||||
|
execute """
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS packets_id_idx
|
||||||
|
ON packets (id)
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
execute "DROP INDEX IF EXISTS packets_received_at_idx"
|
||||||
|
execute "DROP INDEX IF EXISTS packets_id_idx"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -14,8 +14,16 @@ defmodule Aprsme.Workers.PacketCleanupWorkerTest do
|
||||||
days = 30
|
days = 30
|
||||||
job = %Oban.Job{args: %{"cleanup_days" => days}}
|
job = %Oban.Job{args: %{"cleanup_days" => days}}
|
||||||
|
|
||||||
expect(PacketsMock, :clean_packets_older_than, fn ^days ->
|
# Mock the Repo.all call for get_packet_ids_for_deletion
|
||||||
5
|
expect(Aprsme.Repo, :all, fn _query ->
|
||||||
|
# Return some packet IDs
|
||||||
|
[1, 2, 3, 4, 5]
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Mock the Repo.delete_all call
|
||||||
|
expect(Aprsme.Repo, :delete_all, fn _query ->
|
||||||
|
# Return deleted count
|
||||||
|
{5, nil}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert :ok = PacketCleanupWorker.perform(job)
|
assert :ok = PacketCleanupWorker.perform(job)
|
||||||
|
|
@ -26,8 +34,16 @@ defmodule Aprsme.Workers.PacketCleanupWorkerTest do
|
||||||
test "cleans up packets using the default retention period" do
|
test "cleans up packets using the default retention period" do
|
||||||
job = %Oban.Job{args: %{}}
|
job = %Oban.Job{args: %{}}
|
||||||
|
|
||||||
expect(PacketsMock, :clean_old_packets, fn ->
|
# Mock the Repo.all call for get_packet_ids_for_deletion
|
||||||
10
|
expect(Aprsme.Repo, :all, fn _query ->
|
||||||
|
# Return some packet IDs
|
||||||
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Mock the Repo.delete_all call
|
||||||
|
expect(Aprsme.Repo, :delete_all, fn _query ->
|
||||||
|
# Return deleted count
|
||||||
|
{10, nil}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert :ok = PacketCleanupWorker.perform(job)
|
assert :ok = PacketCleanupWorker.perform(job)
|
||||||
|
|
@ -39,11 +55,62 @@ defmodule Aprsme.Workers.PacketCleanupWorkerTest do
|
||||||
days = 60
|
days = 60
|
||||||
deleted_count = 15
|
deleted_count = 15
|
||||||
|
|
||||||
expect(PacketsMock, :clean_packets_older_than, fn ^days ->
|
# Mock the Repo.all call for get_packet_ids_for_deletion
|
||||||
deleted_count
|
expect(Aprsme.Repo, :all, fn _query ->
|
||||||
|
# Return packet IDs
|
||||||
|
Enum.to_list(1..deleted_count)
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Mock the Repo.delete_all call
|
||||||
|
expect(Aprsme.Repo, :delete_all, fn _query ->
|
||||||
|
# Return deleted count
|
||||||
|
{deleted_count, nil}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert ^deleted_count = PacketCleanupWorker.cleanup_packets_older_than(days)
|
assert ^deleted_count = PacketCleanupWorker.cleanup_packets_older_than(days)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "cleanup_packets_older_than_batched/1" do
|
||||||
|
test "returns correct tuple format with deleted count and batches" do
|
||||||
|
days = 30
|
||||||
|
|
||||||
|
# Mock the Repo.all call for get_packet_ids_for_deletion
|
||||||
|
expect(Aprsme.Repo, :all, fn _query ->
|
||||||
|
# Return some packet IDs
|
||||||
|
[1, 2, 3, 4, 5]
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Mock the Repo.delete_all call
|
||||||
|
expect(Aprsme.Repo, :delete_all, fn _query ->
|
||||||
|
# Return deleted count
|
||||||
|
{5, nil}
|
||||||
|
end)
|
||||||
|
|
||||||
|
{deleted_count, batches} = PacketCleanupWorker.cleanup_packets_older_than_batched(days)
|
||||||
|
|
||||||
|
assert is_integer(deleted_count)
|
||||||
|
assert is_integer(batches)
|
||||||
|
assert deleted_count >= 0
|
||||||
|
assert batches >= 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "get_packet_ids_for_deletion/2" do
|
||||||
|
test "returns list of packet IDs within batch size limit" do
|
||||||
|
cutoff_time = DateTime.add(DateTime.utc_now(), -30, :day)
|
||||||
|
batch_size = 100
|
||||||
|
|
||||||
|
# Mock the Repo.all call
|
||||||
|
expect(Aprsme.Repo, :all, fn _query ->
|
||||||
|
# Return some mock packet IDs
|
||||||
|
[1, 2, 3, 4, 5]
|
||||||
|
end)
|
||||||
|
|
||||||
|
result = PacketCleanupWorker.get_packet_ids_for_deletion(cutoff_time, batch_size)
|
||||||
|
|
||||||
|
assert is_list(result)
|
||||||
|
assert length(result) <= batch_size
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue