jonnifyechomq

Time-travel · dive 02

The two reads

Open or closed, both are bound choices over a read that already shipped — and an inverted window never reaches the wire.

The previous dive built the address arithmetic: a %DateTime{} compiles to an exact id bound. This dive composes those bounds into the two public reads. They differ only in the upper edge — one open, one closed — and both delegate to the byte-frozen read/6 (XRANGE). There is no new wire surface and no new Lua: time-travel is two thin functions that pick bounds and hand off. Set a pair of instants on the right and watch the bounds each compiles to — and what happens when you invert the window.

Pick a read · the bounds it builds

from minid_floor(t0) to maxid_ceil(t1) read/6 byte-frozen · XRANGE {:ok, …} mint order closed [t0, t1] both functions delegate to the SAME read/6 — no new Lua

Two windows, one read beneath

The two reads differ only in the upper bound, and both delegate to the same shipped read/6:

read_since(conn, queue, name, t0, count \\ nil) → the open window [t0, ∞). The lower bound is minid_floor(t0); the upper bound is "+", the stream top. “Everything at or after t0.” The half-open lower edge is the exact one the floor already proves: a t0 entry is in, a t0 − 1ms entry is out.

read_window(conn, queue, name, t0, t1, count \\ nil) → the closed window [t0, t1]. The lower bound is minid_floor(t0); the upper bound is maxid_ceil(t1), the inclusive ceiling. “Between t0 and t1, both ends in.”

Both return {:ok, [{branded, fields_map}]} — the entries in mint order, each a {branded EVT id, payload map} tuple, the branded id recovered from the stored id field. A connector or server fault surfaces as {:error, term} verbatim, the same shape read/6 already has.

The two reads, extracted

The real read_since/5 and read_window/6. Notice how little there is to either: each is a bound choice and a delegation. read_window/6 carries one extra guard — the inverted-window check — which it pays before computing any bound.

# echo_mq — EchoMQ.Stream
# OPEN window [t0, ∞): from = minid_floor(t0) (the half-open lower floor), to = "+"
# (the stream top). The common audit case — "everything at or after t0." Delegates
# to the byte-frozen read/6; ZERO new Lua.
def read_since(conn, queue, name, %DateTime{} = t0, count \\ nil)
    when is_binary(queue) and is_binary(name) do
  read(conn, queue, name, minid_floor(t0), "+", count)
end

# CLOSED window [t0, t1], both edges inclusive: from = minid_floor(t0), to =
# maxid_ceil(t1) (the inclusive upper inverse). Delegates to read/6; ZERO new Lua.
# RAISES ArgumentError before any wire on an INVERTED window (t1 strictly before
# t0) — a host-side guard, never a malformed bound to the wire.
def read_window(conn, queue, name, %DateTime{} = t0, %DateTime{} = t1, count \\ nil)
    when is_binary(queue) and is_binary(name) do
  if DateTime.compare(t1, t0) == :lt do
    raise ArgumentError,
          "EchoMQ.Stream.read_window requires t0 <= t1; got t0=#{DateTime.to_iso8601(t0)}, t1=#{DateTime.to_iso8601(t1)}"
  end

  read(conn, queue, name, minid_floor(t0), maxid_ceil(t1), count)
end

The count is an optional cap passed straight through to read/6 (it becomes XRANGE … COUNT n) — for paging a wide window without materializing all of it at once. When nil, the whole window comes back.

An inverted window raises before the wire

read_window/6 checks t1 ≥ t0 first, and raises ArgumentError if not — before it computes a single bound, before it touches the connector. An inverted window is a programming error, not a runtime condition to be handled, and a malformed bound should never reach the wire. This is the same policy-before-existence discipline the append path takes with its kind door and the trim path takes with its name guard: refuse the bad input at the host, loudly, rather than ship a malformed command and let the store's behavior define the meaning. A valid t0 == t1 window is allowed — the degenerate closed window of a single millisecond.

And the window is a filter expressed as bounds. read_window/6 over [t0, t1] returns exactly the entries you would get by reading the whole stream and keeping each one whose branded EVT mint instant falls in [t0, t1] — but it does not read the whole stream and filter in the host. It pushes the filter down to the store as the XRANGE bounds, so the store returns only the slice and the host materializes only the slice. The floor/ceil arithmetic is what makes that push-down exact.

A pair of instants · valid, degenerate, or inverted

time → t0 t1 valid window

Pattern & implementation

the pattern (Redis Patterns Applied)

Read a slice of an event log by a time range — open-ended (“since X”) or closed (“between X and Y”) — with XRANGE over the entry ids. Streams & Events teaches the range read.

the implementation (echo_mq)

read_since/5 ([t0, ∞), to = "+") and read_window/6 ([t0, t1], to = maxid_ceil(t1)) pick bounds from %DateTime{}s and delegate to the byte-frozen read/6; read_window/6 raises on an inverted window before any wire. No new Lua, no new wire surface.

The pattern names two range reads; the implementation is two bound choices over a read that already shipped — the depth is that the bounds are exact and the bad input never sails.

Recap — two reads, one XRANGE

Two reads, one XRANGE beneath. read_since/5 is the open window [t0, ∞); read_window/6 is the closed window [t0, t1], raising ArgumentError on an inverted pair before it touches the wire. Both return mint-ordered {branded, fields_map} tuples and both delegate to the byte-frozen read/6. The next dive puts them to work: backtest, audit, debug.

Next in the module: Backtest, audit, debug — the two reads applied, worked on a codemojex round's activity feed.

References

Where the two reads are grounded, and the range semantics they rest on.

Sources

Related in this course