Why bitemporal?
A typical CAM-table store records “MAC X is on port Y.” A bitemporal one
records “we believed at time R that MAC X was on port Y for the wire-time
range V.” That second clause is the whole point.
Two time axes, not one
Section titled “Two time axes, not one”Bitemporal storage has two independent ranges per row:
valid_during— the wire-time range during which the fact was true on the network. “This MAC was on this port from 10:00 to 10:05.”recorded_during— the belief-time range during which the system believed it. “From 10:00:02 (when we ingested the event) onwards we recorded this as true; we still believe it as of now.”
Most rows have one row per fact: a brand-new observation closes the
previous row’s valid_during and inserts a new row whose recorded_during
opens at now().
The interesting case is belief revision — when a late event arrives
saying “actually, what you believed at T was wrong.” Then we close the
old row’s recorded_during at now() and insert a new row with the
corrected valid_during and a fresh recorded_during. The original
row stays in the log forever; nothing is overwritten.
Concretely
Section titled “Concretely”A timeline of one MAC’s events:
T ─────10:00───10:05───10:10───10:15───10:20──→ [Eth1 ] [Eth2 ]
Event 1 (arrives at 10:00): "MAC seen on Eth1"Event 2 (arrives at 10:05): "MAC seen on Eth2" → closes Eth1's valid_during at 10:05Resulting rows:
entry_id=1 port=Eth1 valid_during=[10:00, 10:05) recorded_during=[10:00, ∞)entry_id=2 port=Eth2 valid_during=[10:05, ∞) recorded_during=[10:05, ∞)Now suppose at 10:20 a late event arrives:
Event 3 (arrives at 10:20): "MAC seen on Eth7 at 10:03" → belief revisionThe old beliefs about [10:00, 10:05) on Eth1 are now contradicted at 10:03.
We close entry_id=1’s recorded_during at 10:20 and insert two new rows:
entry_id=1 port=Eth1 valid_during=[10:00, 10:05) recorded_during=[10:00, 10:20) ← closedentry_id=2 port=Eth2 valid_during=[10:05, ∞) recorded_during=[10:05, ∞)entry_id=3 port=Eth1 valid_during=[10:00, 10:03) recorded_during=[10:20, ∞) ← new belief: Eth1 only until 10:03entry_id=4 port=Eth7 valid_during=[10:03, 10:05) recorded_during=[10:20, ∞) ← new belief: Eth7 insertedNow you can ask either question:
- “What was the path at 10:04, given what we believe today?” —
entry_id=4(port Eth7). - “What was the path at 10:04, given what we believed at 10:18?” —
entry_id=1(port Eth1) —recorded_during=[10:00, 10:20)contains 10:18, andvalid_during=[10:00, 10:05)contains 10:04.
Both answers are correct. Bitemporal lets you ask either.
Why we paid for it
Section titled “Why we paid for it”Most CAM-table stores use one time dimension (“last_seen” + soft “history” that gets overwritten when a new value lands). That works fine for “where is MAC X right now?” but falls apart on:
-
Late arrivals. SNMP polls have a 60-second-or-so floor; SSH snapshot diffs lag worse. A poll at 10:05 reporting “MAC on Eth7 at 10:03” arrives after the streaming-gNMI event “MAC on Eth2 at 10:05.” Single-timestamp models silently overwrite, leaving the bitemporal log saying the MAC was never on Eth7. Worse, the overwrite is undetectable later.
-
Audit and forensic queries. Compliance and “did we know X at the time?” questions need the historical belief, not the current belief. A single-time-axis store cannot answer them.
-
Disagreement detection. Two sources writing into a single-timestamp model fight over the row. Two sources writing into a bitemporal model produce two rows the system can compare and surface.
Vocabulary credit
Section titled “Vocabulary credit”The valid_during / recorded_during naming and the two-axis modelling
in this project mirrors what Martin Fowler called valid time and
transaction time in his “Time Narratives” essay — the canonical
accessible primer on bitemporal data:
If this is your first contact with the model, read Fowler’s essay first; this page picks up where it leaves off and grounds the abstraction in the specifics of MAC tables.
Practical consequences in l2trace
Section titled “Practical consequences in l2trace”- The
mac_observationtable has an EXCLUDE constraint that prevents two currently-believed open observations from overlappingvalid_duringwithin a single source — see the data model reference. - Cross-source disagreement is allowed, by design — surfaced via the disagreement view.
- The TUI’s as-of header drives
valid_during @> :audit_timefilters on every screen — see querying state as of a past time. - Late-arriving events go through a separate code path in the reconciler — see late-arrival classification.