Retention

Aurora Meter Pro keeps financial history for ever. The acknowledgement ledger, the payment accounts, the alert dedupe rows, the reporting aggregates and the source cutovers are never deleted by anything in this package, at any age, under any option, and nothing prunes anything at all until you schedule it.

Five tables grow with traffic rather than with tenants, and each is genuinely disposable once its own condition holds. AuroraMeter.Pro.Retention is that list.

AuroraMeter.Pro.Retention.plan([])
#=> {:ok, %{audit_events: 412_000, outbox_items: 1_204, webhook_events: 8_910,
#=>         recovery_actions: 3, reconciliation_items: 12}}

AuroraMeter.Pro.Retention.prune([])

plan/1 is a dry run. It runs the prune’s own predicate with count(*) instead of a DELETE and writes nothing at all, so the number it gives you is the number prune/1 then removes, generated from one predicate rather than from two that could drift.

The free core has its own list on the same terms, in the page of the same name in its documentation, and neither package prunes the other’s tables. Schedule both.

The allow list

Table Kept for Deleted only when
aurora_meter_audit_events :audit_log_retention_days, 30 days inserted_at is past the cutoff
aurora_meter_outbox_items :outbox_retention_days, 90 days the item is confirmed and confirmed_at is past the cutoff
aurora_meter_webhook_events :webhook_event_retention_days, 30 days inserted_at is past the cutoff
aurora_meter_recovery_actions :recovery_action_retention_days, 365 days inserted_at is past the cutoff
aurora_meter_reconciliation_items :reconciliation_item_retention_days, 365 days the item is resolved and resolved_at is past the cutoff

That is the whole list. There is no option, no configuration key and no argument that adds a table to it. :only narrows the list and can never widen it:

AuroraMeter.Pro.Retention.prune(only: [:audit_events])

AuroraMeter.Pro.Retention.prune(only: [:usage_reports])
#=> ** (ArgumentError) :usage_reports is not a table
#=>    AuroraMeter.Pro.Retention may delete from...

Every window has a floor of one day and a value below it is refused at boot. A retention window is compared against a stored timestamp, and the clocks that write those timestamps step backwards by a few hundred milliseconds to a few seconds; at a day that cannot invert the comparison, and at a minute it can. This decision deletes data that cannot be recovered, so the small value is refused where an operator can still read the message. For a one-off narrower cutoff, pass :older_than_days to a single call.

What is never deleted, and why each one

aurora_meter_usage_reports is the acknowledgement ledger. last_reported per tenant, feature and period is what makes the next delta correct. Delete an old row and the reporter believes that period was never reported and re-sends the whole period’s usage, which is a double bill. Never prunable, at any age.

aurora_meter_credit_accounts is the provider identity of a paying customer and the state of any top-up attempt in flight.

aurora_meter_alerts is a dedupe table: the row is written before the alert is sent and removed only when delivery failed, so deleting an old row re-alerts a customer about a threshold they crossed a year ago. Prunable in principle once the period is long past, and deliberately not: the saving is one row per tenant, feature, period and threshold, and the risk is a customer email.

aurora_meter_rollups is the reporting aggregate the history charts read. Rebuilding it is AuroraMeter.Pro.Rollup‘s job and deleting it is nobody’s.

aurora_meter_source_cutovers holds one row per feature naming the period at which that feature moved from buffered counting to durable events. It is one row per feature, for ever: it does not grow with traffic, and both the reporter and the reconciler read it to decide which source a past period should be billed from. Deleting one would change the commercial answer for every period after it.

An outbox item that is not confirmed is never deleted whatever its age. pending and leased are in flight, accepted and uncertain are waiting on the provider, and rejected and quarantined are records a person still has to look at. An open reconciliation item is never deleted for the same reason.

A test in this package derives the tables the migrations create and fails unless every one appears in exactly one of the two lists, in both directions. A table nobody classified is a table that grows for ever, or worse, one a later prune reaches.

Why 90 days for a confirmed outbox item

A confirmed item is a settled fact: the provider accepted it and a later reconciliation cycle found a summary that accounts for it. The evidence that it happened does not live only in that row. aurora_meter_usage_reports keeps the acknowledged position for that tenant, feature and period for ever, and the provider keeps its own record. Ninety days is comfortably longer than the reconciler’s lookback, so nothing this package does reads a confirmed item older than that.

If you would rather keep them, set outbox_retention_days higher, or leave the table out of the schedule with :only. Nothing breaks either way; the table grows.

Scheduling

config :my_app, Oban,
  queues: [aurora_meter: 5],
  plugins: [{Oban.Plugins.Cron, crontab: [
    {"30 3 * * *", AuroraMeter.Pro.Retention.Pruner}
  ]}]

AuroraMeter.Pro.cron_entries/1 includes it. "30 3 * * *" is the schedule AuroraMeter.Pro.AuditLog.Pruner documented before this worker existed, so a host moving the entry across keeps its existing time.

AuroraMeter.Pro.AuditLog.Pruner is deprecated and still works: it prunes the audit log and only the audit log, exactly as it did. Move the crontab entry, and do not register both, because both sweep the audit log.

Pausing

One operation per table:

AuroraMeter.Operations.pause("pro_retention:outbox_items")
AuroraMeter.Operations.resume("pro_retention:outbox_items")

A paused table is reported as a :paused reason rather than as a failure, and the other tables are still pruned.

When one run cannot keep up

prune/1 removes at most :max_items rows per table per call (default 50,000), so it cannot run for an unbounded time. A run that hits that bound with rows still eligible says so:

AuroraMeter.Pro.Retention.prune([])
#=> {:blocked, %{audit_events: 50_000, ...},
#=>  [%{table: :audit_events, reason: :budget_exhausted,
#=>     detail: %{deleted: 50_000, max_items: 50_000, batch_size: 1000}}]}

That is not an error. It is the signal that the table is growing faster than one nightly run removes, and the two answers are to raise :max_items or to run the worker more often. plan/1 tells you how much is waiting.

The audit log is usually the one that needs this: it is one row per API request, so a busy installation can add more rows in a day than a default run removes.

Archiving

Aurora Meter Pro uploads nothing anywhere and ships no archiver. Building one would be a second storage system, and you already have a better one: your backups.

If you want a copy of what a prune is about to remove, take it with COPY before the prune:

COPY (SELECT * FROM aurora_meter_audit_events
       WHERE inserted_at < now() - interval '30 days')
  TO '/var/backups/aurora_audit_events.csv' WITH CSV HEADER;

For the tables that are never pruned, which is where your commercial history lives, the archive is your ordinary database backup and nothing here changes that. If you want them out of the hot database for size reasons, that is a partitioning or a warehouse question rather than a retention one: a usage report or a source cutover that is gone cannot be reconciled against the provider afterwards.