AuroraMeter.Events.Outbox behaviour (Aurora Meter v1.0.0)

View Source

The one seam between a recorded event and anything that delivers it.

AuroraMeter.record/4 calls enqueue/2 inside the transaction that writes the event row and its projection delta. That is the whole point: an export intent that is staged in the same commit as the fact cannot be lost, and cannot exist for a fact that was rolled back. Configure it with

config :aurora_meter, events_outbox: MyApp.Outbox

Core ships AuroraMeter.Events.Outbox.Noop, which is the default and does nothing. Core has no delivery of its own and knows nothing about Stripe, customers, meters or modes; Aurora Meter Pro implements this behaviour and owns every provider-shaped decision.

The contract an implementation must keep

  • Use the repo you are given. context.repo is the repo running the open transaction. Checking out another connection puts your write outside the commit that carries the fact, which is the one thing this seam exists to prevent.
  • Be fast and do no I/O off the database. You are inside a financial transaction that also holds a shared lock on the projection checkpoint row. A network call here stalls every concurrent record/4.
  • Reserve errors for storage failures. Returning {:error, reason} or raising rolls the caller's record/4 back and the caller sees {:error, {:unavailable, {:outbox, reason}}}. Anything you can express as a quarantined item should be enqueued as one and reported later, not turned into a refusal to record the usage.

Eligibility

Each item carries what core knows about whether the event is a candidate for delivery, and nothing else:

  • :eligible
  • {:ineligible, :feature_buffered} when the feature reports from the buffered counter rather than from events (build unit 03c)
  • {:ineligible, :attribution_unresolved} when the period source could not place occurred_at, so the period on the row is an approximation
  • {:ineligible, :plan_unresolved} when the period is a fact but no plan assignment covers occurred_at, so the row names no commercial contract (build unit 07c). The item is never delivered on a guess: substituting today's plan for a historical fact is the repricing decision D05 forbids.
  • {:ineligible, :original_ineligible} on a correction whose original carried an unresolved attribution, of either kind. The correction of an unattributed fact cannot be attributed either, and the reason names which of the two rows is the problem.

Mapping, customer, mode, meter, cutover watermark and timestamp window are the implementation's decisions, taken at enqueue time from its own configuration.

A correction is never dropped. Core stages every one of them, with a reason attached when core can already tell delivery is not possible, because a correction that quietly disappears is a customer credited nothing and told otherwise (I09).

Summary

Types

What the record transaction hands the implementation.

Why core believes an event is not a candidate for delivery.

One export intent.

Callbacks

Stages the export intent for every accepted event, inside the caller's transaction.

Types

context()

@type context() :: %{repo: module(), timeout: timeout()}

What the record transaction hands the implementation.

ineligibility()

@type ineligibility() ::
  :feature_buffered
  | :attribution_unresolved
  | :plan_unresolved
  | :original_ineligible

Why core believes an event is not a candidate for delivery.

item()

@type item() :: %{
  event: AuroraMeter.Event.t(),
  eligibility: :eligible | {:ineligible, ineligibility()}
}

One export intent.

Callbacks

enqueue(list, context)

@callback enqueue([item()], context()) :: :ok | {:error, term()}

Stages the export intent for every accepted event, inside the caller's transaction.

Duplicates are not passed: an identity that was already recorded produced its intent when it was first recorded.