AuroraMeter. Events. Backfill
(Aurora Meter v1.0.0)
View Source
Internal. The implementation behind mix aurora_meter.events.backfill.
Not part of the supported surface; run the Mix task.
It gives every aurora_meter_events row written before core schema version 7
the identity that version 8 makes mandatory. Bounded, checkpointed and
re-runnable.
What it writes, and what it approximates
For each row whose event_id is null:
event_id = "legacy:" <> id, derived from the row's own primary key, so two runs produce the same value for the same row and a re-run is a no-op.occurred_at = inserted_at. This is an approximation. The row was written byAuroraMeter.track/4, which never recorded when the usage happened, only when the row reached the database.kind = "usage",original_event_id = NULL,dimensions = '{}'.period_startandperiod_sourcefromAuroraMeter.Period.containing/2atoccurred_at.payload_hashfrom the canonical tuple (AuroraMeter.Events.Canonical).attribution = "resolved"when the period source placed the instant, and"unresolved"when it could not, in which caseperiod_startis the calendar month containing the instant. That is the second approximation, and"unresolved"is how you find it.plan_idandplan_versionstay null. Legacy events were never billed and their plan attribution cannot be reconstructed.
How an operator tells an approximated row from an exact one
-- every row this task has ever touched: occurred_at is the write time,
-- not the usage time
SELECT count(*) FROM aurora_meter_events WHERE event_id LIKE 'legacy:%';
-- of those, the ones whose billing period is a guess as well
SELECT count(*) FROM aurora_meter_events WHERE attribution = 'unresolved';A row recorded through the V1 durable path carries a caller-supplied
event_id that cannot begin legacy: (the prefix is reserved) and an
occurred_at the caller stated. The legacy: prefix is therefore the exact
discriminator, and it is derived rather than flagged, so it cannot drift.
What it never writes
It writes no aurora_meter_event_totals row, enqueues no outbox item and
emits no PubSub (L-03a-3). Legacy events were never billed, and upgrading one
must not make it billable.
Exclusion between two runners
Two concurrent runs are safe, because the derivation is deterministic and
every update carries WHERE event_id IS NULL; they are only wasteful. The
task still refuses a second run, and it decides that with a Postgres
advisory lock on a pinned connection, held for the length of the run.
A lock rather than a lease, deliberately. A lease is a duration compared
against a clock, and clock_timestamp() is the database host's OS clock,
which is corrected: it was measured stepping backwards 439 ms on a 32.5
second cadence on the development host (open-findings.md X100). A lock has
no clock in it, and it releases itself when the connection holding it dies,
which is exactly the signal a stale lease is trying to approximate.
The checkpoint row's state and updated_at are a report for an
operator, not the decision. A "running" state with the lock free means a
previous run was killed; the task says so and --force-resume proceeds.
One measured consequence of holding the lock on a pooled connection: when the
runner is killed, Postgres releases the lock as the connection closes, and
the pool closes it when it notices the client is gone. That is immediate for
a killed OS process, and on this host it took under 50 ms for a killed BEAM
process. So a resume attempted in the same instant as the kill can still see
:already_running; a resume attempted by a human cannot.
Resuming
The cursor is %{"seq" => n} in aurora_meter_checkpoints["events_backfill"],
advanced inside the same transaction as the batch it describes. A run killed
between batches resumes at the last committed batch. The scan is ordered by
seq, never by id: id is a random v4 UUID, so a forward keyset scan
ordered by it can miss a row committed by a transaction that started earlier.
A row inserted by a still-running 0.4.x node during the backfill receives a
higher seq than the cursor, so the same forward pass reaches it.
Summary
Functions
The name of the checkpoint row this task keeps.
The event_id this task derives for a legacy row with the given primary key.
How many rows still have no event_id. What core schema version 8 refuses on.
Runs the backfill.
Types
The counts a run reports.
@type result() :: {:ok, counts()} | {:error, :already_running, counts()} | {:error, :stale_running, counts()} | {:error, :paused, counts()}
What a run returns.
:already_running means another process holds the advisory lock right now.
:stale_running means the checkpoint says "running" but nothing holds the
lock, so the previous run was killed: --force-resume proceeds from its
cursor. The two are kept apart because only the second is a decision for a
human.
Functions
@spec checkpoint_name() :: String.t()
The name of the checkpoint row this task keeps.
Examples
iex> AuroraMeter.Events.Backfill.checkpoint_name()
"events_backfill"
The event_id this task derives for a legacy row with the given primary key.
Deterministic, which is what makes a re-run a no-op.
Examples
iex> AuroraMeter.Events.Backfill.legacy_event_id("6d1f0b9c-0000-4000-8000-000000000001")
"legacy:6d1f0b9c-0000-4000-8000-000000000001"
@spec remaining(keyword()) :: non_neg_integer()
How many rows still have no event_id. What core schema version 8 refuses on.
Options: :repo.
Runs the backfill.
Options:
:repo: the repo to work through. Defaults to the configured repo.:batch_size: rows per transaction, default 5000.:max_batches: stop cleanly after this many committed batches, leaving the checkpoint at the last one. For a bounded maintenance window.:dry_run: scan and compute, write nothing at all, report the same counts. No row is updated and no checkpoint row is created or changed, so a dry run takes no lock and is safe beside a real one.:force_resume: proceed although the checkpoint says"running". Only reachable when the advisory lock is free, which is to say when the previous runner is gone.:timeout: milliseconds allowed for one statement and for one batch transaction, default 60000. Raise it with--batch-size, or a large batch on a busy database dies part way through.