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
| Question | Callback | Promise |
|---|---|---|
What period is this? What do I display? What goes in inserted_at? | now/0 | Wall-clock shaped and cheap. It promises nothing about monotonicity and may step backwards. |
| What UTC day is this? | today/0 | The 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/0 | Strictly monotone within a node. Never persisted, never compared across nodes. |
| Has enough time passed since something persisted? | db_now/0 | The 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/0returns aDateTimeinEtc/UTCwith microsecond precision. It is on theAuroraMeter.track/4path, so it must be pure and cheap.today/0returns theDateof that same instant in UTC. Derive it fromnow/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/0returns an integer in milliseconds from an arbitrary origin. Only differences between two readings are meaningful.db_now/0returns aDateTimeinEtc/UTCfrom 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.
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
@callback db_now() :: DateTime.t()
The database's current instant: the one clock every node shares.
@callback monotonic_ms() :: integer()
A strictly monotonic millisecond reading, for in-memory elapsed spans only.
@callback now() :: DateTime.t()
The current instant, in Etc/UTC with microsecond precision.
@callback today() :: Date.t()
The UTC date of now/0.
Functions
@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"
@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
@spec now() :: DateTime.t()
The current instant from the configured clock.
Examples
iex> AuroraMeter.Clock.now().time_zone
"Etc/UTC"
@spec today() :: Date.t()
The current UTC date from the configured clock.
Examples
iex> match?(%Date{}, AuroraMeter.Clock.today())
true