AuroraMeter.Clock behaviour (Aurora Meter v1.0.0)

View Source

The single source of "now" for Aurora Meter.

Every instant and every date in lib/ comes from here, so a test can freeze time and so the library never depends on a primitive that can go backwards. The module configured under clock: (default AuroraMeter.Clock.System) implements the four callbacks below; hosts that want to freeze time in their own tests use AuroraMeter.Test.with_clock/2.

Four questions, four readings

QuestionCallbackPromise
What period is this? What do I display? What goes in inserted_at?now/0Wall-clock shaped and cheap. It promises nothing about monotonicity and may step backwards.
What UTC day is this?today/0The date of now/0, never a second clock read.
How long has this in-memory thing taken? (a cache TTL, a timeout, a span inside one process)monotonic_ms/0Strictly monotone within a node. Never persisted, never compared across nodes.
Has enough time passed since something persisted?db_now/0The one clock every node in a cluster shares. Costs a round trip, so it is never on the AuroraMeter.track/4 path.

The rule

Stamp and compare with the same clock, and for anything persisted that clock is the database's.

A decision that compares "now" against a timestamp read out of a row takes db_now/0, and that row's timestamp must itself have been stamped by the database, with a fragment("clock_timestamp()") or a column default. The defect is comparing a stamp from one clock against a reading from another; the size of any single clock error is not the point.

Two things this does not claim. db_now/0 does not make the database's clock perfect: that host's clock can be corrected too. What it removes is the skew between two clocks, and it leaves a single, operationally managed clock in its place. And ordering never takes a clock at all: it takes a sequence number. See docs/periods.md.

Contract for an implementation

  • now/0 returns a DateTime in Etc/UTC with microsecond precision. It is on the AuroraMeter.track/4 path, so it must be pure and cheap.
  • today/0 returns the Date of that same instant in UTC. Derive it from now/0; reading a second clock opens a window where an increment at midnight lands in the period of one day and the bucket of another.
  • monotonic_ms/0 returns an integer in milliseconds from an arbitrary origin. Only differences between two readings are meaningful.
  • db_now/0 returns a DateTime in Etc/UTC from the database the host configured. It may be slow and it may raise; no hot path calls it.

Summary

Callbacks

The database's current instant: the one clock every node shares.

A strictly monotonic millisecond reading, for in-memory elapsed spans only.

The current instant, in Etc/UTC with microsecond precision.

The UTC date of now/0.

Functions

The database's current instant, from the configured clock.

A monotonic millisecond reading from the configured clock.

The current instant from the configured clock.

The current UTC date from the configured clock.

Callbacks

db_now()

@callback db_now() :: DateTime.t()

The database's current instant: the one clock every node shares.

monotonic_ms()

@callback monotonic_ms() :: integer()

A strictly monotonic millisecond reading, for in-memory elapsed spans only.

now()

@callback now() :: DateTime.t()

The current instant, in Etc/UTC with microsecond precision.

today()

@callback today() :: Date.t()

The UTC date of now/0.

Functions

db_now()

@spec db_now() :: DateTime.t()

The database's current instant, from the configured clock.

Use it for every comparison against a timestamp that came out of a row, and make sure that row's timestamp was stamped by the database too. It costs a round trip: never call it from AuroraMeter.track/4, check/2, reserve/2,3 or with_quota/3,4.

Examples

iex> AuroraMeter.Clock.db_now().time_zone
"Etc/UTC"

monotonic_ms()

@spec monotonic_ms() :: integer()

A monotonic millisecond reading from the configured clock.

Use it for an elapsed span that begins and ends inside one node's memory. Never persist it and never compare it across nodes.

Examples

iex> is_integer(AuroraMeter.Clock.monotonic_ms())
true

now()

@spec now() :: DateTime.t()

The current instant from the configured clock.

Examples

iex> AuroraMeter.Clock.now().time_zone
"Etc/UTC"

today()

@spec today() :: Date.t()

The current UTC date from the configured clock.

Examples

iex> match?(%Date{}, AuroraMeter.Clock.today())
true