AuroraMeter. Oban. CreditExpiry
(Aurora Meter v1.0.0)
View Source
Expires promotional credit grants whose date has passed, in bounded batches.
The work is AuroraMeter.Credits.expire_due/2 and nothing else. The worker
opens no transaction, takes no lock and reads no clock to decide anything:
expire_due/2 compares against a persisted expires_at, so the instant it
compares with comes from the database (AuroraMeter.Clock).
config :my_app, Oban,
plugins: [{Oban.Plugins.Cron, crontab: [
{"*/30 * * * *", AuroraMeter.Oban.CreditExpiry}
]}]Recommended schedule "*/30 * * * *", which is what
AuroraMeter.Oban.cron_entries/1 returns. Nothing depends on the frequency
for correctness: a grant expires the first time a run sees it is due, and
the delay between the date passing and the run is the schedule's period.
Job arguments
| Key | Default | Meaning |
|---|---|---|
"batch_size" | 200 | Grants examined per batch. |
"max_batches" | 10 | Batches one job runs before returning. |
Both are bounded integers and neither is a cursor. The cursor is never a
job argument (L05c-1): it lives in aurora_meter_checkpoints under
"credit_expiry:global", so two jobs for this worker read the same position
and the second one resumes where the first got to rather than restarting
from a stale copy of its own.
Pausing
AuroraMeter.Operations.pause("credit_expiry:global")The next batch boundary cancels the job with {:cancel, :paused}, leaving
the cursor where the last committed batch left it.
AuroraMeter.Operations.resume/1 and the next tick carry on from there.
The scan's instant is pinned
The candidate set is expires_at <= now, which moves. A scan that spans
several jobs stores the now it started with in its checkpoint beside the
cursor and hands the same one back until the scan finishes, so a resumed
cursor points into the set it came from. When the scan completes the cursor
is cleared and the next run takes a fresh instant.
Running it twice
Harmless, and that is the operation's doing rather than this worker's. Two
ticks of one minute, a rescued job, or two nodes sweeping at the same
instant all call expire_due/2 twice. Each expiry re-reads expired_at
under the grant row's own FOR UPDATE and refuses when it is already set,
so the second caller expires nothing and counts it as skipped.
The unique option below is defence in depth and not the guarantee. It
saves the duplicate run's work; it is not what keeps the ledger right.
Without Oban, call AuroraMeter.Credits.expire_due/2 from whatever
scheduler you do have, or drive the same loop with
AuroraMeter.Operations.run_batches/3. See the scheduler
map.
Summary
Functions
The checkpoint name this worker pauses and resumes under.
Expires due grants in bounded batches and returns {:ok, report}.
Functions
@spec operation() :: String.t()
The checkpoint name this worker pauses and resumes under.
@spec perform(Oban.Job.t()) :: {:ok, term()} | {:error, term()} | {:cancel, term()}
Expires due grants in bounded batches and returns {:ok, report}.
{:cancel, :paused} when the operation is paused; {:error, reason} when
the listing or the checkpoint write fails, which fails the job and lets Oban
retry it.
Examples
{:ok, report} = AuroraMeter.Oban.CreditExpiry.perform(%Oban.Job{args: %{}})