LCOV - code coverage report
Current view: top level - antares-api/src - history.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 94.7 % 738 699
Test Date: 2026-11-02 13:21:27 Functions: 60.1 % 368 221

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! The temporal seam's producer side (ADR-0013): the write path pushes
       3              : //! `TemporalEvent`s into a per-request buffer; the buffer is drained ONCE
       4              : //! per request — after the handler, before the response leaves — so the
       5              : //! driver sees the whole request in one `event_list` call and a client
       6              : //! reading its own history right after the write always finds it.
       7              : //!
       8              : //! Outside a request (background jobs, tests without the router) there is
       9              : //! no buffer, and a push drains immediately: the seam degrades to today's
      10              : //! per-change recording rather than losing events. A driver error in the
      11              : //! drain is logged and counted (`/q/health` temporalDrainErrors) — it never
      12              : //! changes the response of a write that already committed.
      13              : 
      14              : use crate::state::{AppState, RecordMode};
      15              : use antares_model::TenantId;
      16              : use antares_store::{TemporalDriverExt as _, TemporalEvent};
      17              : use serde_json::{Map, Value};
      18              : use std::sync::atomic::{AtomicU64, Ordering};
      19              : use std::time::Duration;
      20              : 
      21              : static DRAIN_ERRORS: AtomicU64 = AtomicU64::new(0);
      22              : 
      23              : /// Drains that failed in the driver (the events of that request are lost;
      24              : /// the write itself stood).
      25          102 : pub fn drain_errors() -> u64 {
      26          102 :     DRAIN_ERRORS.load(Ordering::Relaxed)
      27          102 : }
      28              : 
      29              : // The buffer rides the request's task; tokio task-locals need the `rt`
      30              : // feature the single-threaded wasm build does not carry, so wasm records
      31              : // immediately.
      32              : #[cfg(not(target_arch = "wasm32"))]
      33              : tokio::task_local! {
      34              :     static BUFFER: std::cell::RefCell<Vec<TemporalEvent>>;
      35              :     static STAGED: std::cell::RefCell<Vec<TemporalEvent>>;
      36              :     static CHANGES: std::cell::RefCell<Vec<crate::mirror::Change>>;
      37              :     static PROVENANCE: Value;
      38              :     static TENANT: Option<TenantId>;
      39              : }
      40              : 
      41              : /// `ANTARES_HISTORY_BACKFILL=on`: a Core API write of
      42              : /// an instance older than the one the Entity holds goes to history only.
      43              : /// Off (the default) keeps NGSI-LD's replace, which the ETSI suites assume.
      44        51572 : fn backfill_on() -> bool {
      45              :     static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
      46        51572 :     *ON.get_or_init(|| std::env::var("ANTARES_HISTORY_BACKFILL").as_deref() == Ok("on"))
      47        51572 : }
      48              : 
      49              : /// Whether `incoming`, about to replace `current` of `attr` on the Entity
      50              : /// `doc` (expanded, as stored), is a backfill: both observed and `incoming`
      51              : /// the older. A backfill is recorded into the request's history here, and
      52              : /// the caller keeps `current`. Outside a request, or with backfill off,
      53              : /// nothing is one.
      54        51572 : pub(crate) fn backfilled(doc: &Value, attr: &str, current: &Value, incoming: &Value) -> bool {
      55        51572 :     if !backfill_on() {
      56        51562 :         return false;
      57           10 :     }
      58           20 :     let at = |i: &Value| {
      59           20 :         i.get("observedAt").and_then(Value::as_str).and_then(|s| {
      60           20 :             chrono::DateTime::parse_from_rfc3339(&antares_store::filter::canonical_datetime(s)).ok()
      61           20 :         })
      62           20 :     };
      63           10 :     let (Some(was), Some(now)) = (at(current), at(incoming)) else {
      64            0 :         return false;
      65              :     };
      66           10 :     let Some(id) = doc.get("id").and_then(Value::as_str) else {
      67            0 :         return false;
      68              :     };
      69           10 :     if now >= was {
      70            2 :         return false;
      71            8 :     }
      72              :     #[cfg(target_arch = "wasm32")]
      73              :     let _ = (attr, id);
      74              :     #[cfg(not(target_arch = "wasm32"))]
      75              :     {
      76            8 :         let Ok(Some(tenant)) = TENANT.try_with(Clone::clone) else {
      77            0 :             return false;
      78              :         };
      79            8 :         let mut instance = incoming.clone();
      80            8 :         if let Some(o) = instance.as_object_mut() {
      81            8 :             let iid = crate::notify::instance_id(id, attr, o);
      82            8 :             o.entry("instanceId".to_owned())
      83            8 :                 .or_insert(Value::String(iid));
      84            8 :             o.insert(
      85            8 :                 antares_store::PROVENANCE_KEY.to_owned(),
      86            8 :                 provenance("backfill"),
      87            8 :             );
      88            8 :         }
      89            8 :         let mut shell = Map::new();
      90           40 :         for k in ["id", "type", "createdAt", "modifiedAt", "scope"] {
      91           40 :             if let Some(v) = doc.get(k) {
      92           16 :                 shell.insert(k.to_owned(), v.clone());
      93           24 :             }
      94              :         }
      95            8 :         let event = TemporalEvent {
      96            8 :             op: antares_store::TemporalOp::AttrModified,
      97            8 :             tenant,
      98            8 :             entity_id: id.to_owned(),
      99            8 :             shell: Value::Object(shell),
     100            8 :             attr: attr.to_owned(),
     101            8 :             instance,
     102            8 :         };
     103            8 :         stage(event)
     104              :     }
     105              :     #[cfg(target_arch = "wasm32")]
     106              :     false
     107        51572 : }
     108              : 
     109              : /// Holds a backfill's event until its write is known to commit: the
     110              : /// closure that found it may still be refused or rerun, and
     111              : /// [`settle`] buffers it only then. `false` outside a request.
     112              : #[cfg(not(target_arch = "wasm32"))]
     113           24 : fn stage(event: TemporalEvent) -> bool {
     114           24 :     STAGED.try_with(|b| b.borrow_mut().push(event)).is_ok()
     115           24 : }
     116              : 
     117              : /// A read-modify-write of Entity `id` (re)starts: the backfills an earlier
     118              : /// run of its closure staged are void. Called first thing in
     119              : /// every closure that can reach [`backfilled`].
     120        13952 : pub(crate) fn restage(id: &str) {
     121              :     #[cfg(not(target_arch = "wasm32"))]
     122        13952 :     let _ = STAGED.try_with(|b| b.borrow_mut().retain(|e| e.entity_id != id));
     123              :     #[cfg(target_arch = "wasm32")]
     124              :     let _ = id;
     125        13952 : }
     126              : 
     127              : /// The read-modify-write of Entity `id` returned: its staged backfills go
     128              : /// to the request's history when the write committed, and are dropped when
     129              : /// it was refused or found nothing.
     130        14050 : pub(crate) fn settle(id: &str, committed: bool) {
     131              :     #[cfg(not(target_arch = "wasm32"))]
     132              :     {
     133        14050 :         let mine = STAGED
     134        14050 :             .try_with(|b| {
     135        14050 :                 let mut b = b.borrow_mut();
     136        14050 :                 let (mine, rest) = std::mem::take(&mut *b)
     137        14050 :                     .into_iter()
     138        14050 :                     .partition::<Vec<_>, _>(|e| e.entity_id == id);
     139        14050 :                 *b = rest;
     140        14050 :                 mine
     141        14050 :             })
     142        14050 :             .unwrap_or_default();
     143        14050 :         if committed && !mine.is_empty() {
     144           12 :             let _ = BUFFER.try_with(|b| b.borrow_mut().extend(mine));
     145        14038 :         }
     146              :     }
     147              :     #[cfg(target_arch = "wasm32")]
     148              :     let _ = (id, committed);
     149        14050 : }
     150              : 
     151              : /// [`settle`] for each id of a batch read-modify-write, by its slot.
     152          172 : pub(crate) fn settle_all<T, E>(ids: &[String], res: &[Option<Result<T, E>>]) {
     153          288 :     for (id, r) in ids.iter().zip(res) {
     154          288 :         settle(id, matches!(r, Some(Ok(_))));
     155              :     }
     156          172 : }
     157              : 
     158              : /// The provenance of the request in flight with its `operation` and
     159              : /// `writtenAt`, when the broker stored it; outside a
     160              : /// request, a write of the broker's own: `api`.
     161       132004 : pub(crate) fn provenance(operation: &str) -> Value {
     162              :     #[cfg(not(target_arch = "wasm32"))]
     163       132004 :     let request = PROVENANCE.try_with(Clone::clone).ok();
     164              :     #[cfg(target_arch = "wasm32")]
     165              :     let request: Option<Value> = None;
     166       132004 :     let base = request.unwrap_or_else(|| serde_json::json!({"channel": "api"}));
     167       132004 :     let mut out = crate::provenance::with_operation(&base, operation);
     168       132004 :     if let Some(o) = out.as_object_mut() {
     169       132004 :         let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
     170       132004 :         o.insert("writtenAt".into(), Value::String(now));
     171       132004 :     }
     172       132004 :     out
     173       132004 : }
     174              : 
     175              : /// Whether the request in flight is audited: each change is then kept as a version
     176              : /// whatever the type's history rule says.
     177       319478 : fn audited() -> bool {
     178              :     #[cfg(not(target_arch = "wasm32"))]
     179       319478 :     return PROVENANCE
     180       319478 :         .try_with(crate::provenance::audited)
     181       319478 :         .unwrap_or(false);
     182              :     #[cfg(target_arch = "wasm32")]
     183              :     false
     184       319478 : }
     185              : 
     186              : /// Buffer one entity change for the request in flight so the matcher
     187              : /// receives the whole request at once. Handed back when no request is in
     188              : /// flight — the caller then gives it to the matcher on the spot.
     189       140898 : pub(crate) fn buffer_change(change: crate::mirror::Change) -> Option<crate::mirror::Change> {
     190              :     #[cfg(not(target_arch = "wasm32"))]
     191              :     {
     192       140898 :         let mut slot = Some(change);
     193       140898 :         let _ = CHANGES.try_with(|b| {
     194        27868 :             if let Some(c) = slot.take() {
     195        27868 :                 b.borrow_mut().push(c);
     196        27868 :             }
     197        27868 :         });
     198       140898 :         slot
     199              :     }
     200              :     #[cfg(target_arch = "wasm32")]
     201              :     Some(change)
     202       140898 : }
     203              : 
     204              : /// Hand one event to the seam: buffered when a request is in flight,
     205              : /// drained on the spot otherwise.
     206       131624 : pub(crate) async fn push(st: &AppState, ev: TemporalEvent) {
     207              :     #[cfg(not(target_arch = "wasm32"))]
     208       112394 :     let ev = {
     209       131624 :         let mut slot = Some(ev);
     210       131624 :         let buffered = BUFFER
     211       131624 :             .try_with(|b| {
     212        19230 :                 if let Some(ev) = slot.take() {
     213        19230 :                     b.borrow_mut().push(ev);
     214        19230 :                 }
     215        19230 :             })
     216       131624 :             .is_ok();
     217       131624 :         if buffered {
     218        19230 :             return;
     219       112394 :         }
     220       112394 :         match slot {
     221       112394 :             Some(ev) => ev,
     222            0 :             None => return,
     223              :         }
     224              :     };
     225       112394 :     drain(st, vec![ev]).await;
     226       131624 : }
     227              : 
     228              : /// The gate chain: an event enters history only if every gate admits it.
     229              : /// Gate 1 (value-change) runs in the producer — an unchanged instance never
     230              : /// becomes an event (`changed_instances`) — and so does gate 3, retention
     231              : /// ([`records_history`]), which needs the whole Entity. Adding a gate = one more entry
     232              : /// here; producers and drivers stay untouched.
     233              : const GATES: &[fn(&AppState, &TemporalEvent) -> bool] = &[observed_gate];
     234              : 
     235              : /// Gate 2: ANTARES_TEMPORAL_RECORD. `all` admits everything; `observed`
     236              : /// keeps only instances that carry `observedAt` — the spec's own
     237              : /// measurement axis (4.5.7: observedAt is the default timeproperty), so
     238              : /// metadata-shaped writes leave no history; `none` admits nothing.
     239       131632 : fn observed_gate(st: &AppState, ev: &TemporalEvent) -> bool {
     240              :     use crate::state::TemporalRecord::*;
     241              :     // An audited write is kept without `observedAt` too.
     242       131632 :     let audited = || {
     243           20 :         ev.instance
     244           20 :             .get(antares_store::PROVENANCE_KEY)
     245           20 :             .is_some_and(crate::provenance::audited)
     246           20 :     };
     247       131632 :     match st.temporal_record {
     248       131570 :         All | Ruled => true,
     249           48 :         Observed => ev.instance.get("observedAt").is_some() || audited(),
     250           14 :         None => false,
     251              :     }
     252       131632 : }
     253              : 
     254              : /// An Entity's own history retention, read from its
     255              : /// [`antares_store::HISTORY_RETENTION`] Property.
     256              : #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     257              : pub enum OwnRetention {
     258              :     /// `off`: the Entity keeps its current state only.
     259              :     Off,
     260              :     /// A duration: its history is kept that long, whatever its type says.
     261              :     Keep(Duration),
     262              : }
     263              : 
     264              : /// What the Entity document `doc` (expanded, as stored) says of its own
     265              : /// history: `None` when it carries no `historyRetention`, `Some(Err)` with
     266              : /// the reason when the value is neither a duration nor `off` (a typo,
     267              : /// `forever`), which the rules of its type then override.
     268       137612 : pub fn own_retention(doc: &Value) -> Option<Result<OwnRetention, String>> {
     269       137612 :     doc.get(antares_store::HISTORY_RETENTION).map(retention_of)
     270       137612 : }
     271              : 
     272              : /// What one stored `historyRetention` Property (one instance, or an array
     273              : /// whose first instance counts) says.
     274              : ///
     275              : /// # Errors
     276              : /// Why it is neither a duration nor `off`.
     277           36 : pub fn retention_of(own: &Value) -> Result<OwnRetention, String> {
     278           36 :     let first = match own {
     279            8 :         Value::Array(all) => all.first(),
     280           28 :         one => Some(one),
     281              :     };
     282           36 :     let Some(raw) = first.and_then(|p| p.get("value")).and_then(Value::as_str) else {
     283            8 :         return Err("historyRetention is not a Property with a string value".into());
     284              :     };
     285           28 :     if raw == "off" {
     286            8 :         Ok(OwnRetention::Off)
     287              :     } else {
     288           20 :         parse_duration(raw).map(OwnRetention::Keep)
     289              :     }
     290           36 : }
     291              : 
     292              : /// Gate 3, run in the producer where the whole Entity is at
     293              : /// hand: whether a write of `doc` in `tenant` records history. The most
     294              : /// specific level wins: the Entity's own `historyRetention`, then the rule
     295              : /// of one of its types (any `off` among them records nothing), then the
     296              : /// Tenant's `*` rule. Below them all, `ANTARES_TEMPORAL_RECORD=ruled`
     297              : /// records nothing (history is opt-in), any other mode everything.
     298       137596 : pub(crate) fn records_history(st: &AppState, tenant: &TenantId, doc: &Value) -> bool {
     299       137596 :     if audited() {
     300            0 :         return true;
     301       137596 :     }
     302       137596 :     match own_retention(doc) {
     303            4 :         Some(Ok(OwnRetention::Off)) => return false,
     304            8 :         Some(Ok(OwnRetention::Keep(_))) => return true,
     305       137584 :         Some(Err(_)) | None => {}
     306              :     }
     307       137584 :     let rules = st
     308       137584 :         .history_off
     309       137584 :         .read()
     310       137584 :         .unwrap_or_else(std::sync::PoisonError::into_inner);
     311       137584 :     let opt_in = st.temporal_record == crate::state::TemporalRecord::Ruled;
     312       137584 :     if rules.is_empty() && !opt_in {
     313       137522 :         return true;
     314           62 :     }
     315           62 :     let tenant = tenant.as_str();
     316           62 :     let types: Vec<&str> = match doc.get("type") {
     317           40 :         Some(Value::String(t)) => vec![t.as_str()],
     318           18 :         Some(Value::Array(ts)) => ts.iter().filter_map(Value::as_str).collect(),
     319            4 :         _ => Vec::new(),
     320              :     };
     321          112 :     let has = |set: &std::collections::HashSet<(String, String)>| {
     322          112 :         types
     323          112 :             .iter()
     324          112 :             .any(|t| set.contains(&(tenant.to_owned(), (*t).to_owned())))
     325          112 :     };
     326           62 :     if has(&rules.off) {
     327           12 :         return false;
     328           50 :     }
     329           50 :     if opt_in {
     330           30 :         return has(&rules.ruled) || rules.space_on.contains(tenant);
     331           20 :     }
     332           20 :     has(&rules.ruled) || !rules.space_off.contains(tenant)
     333       137596 : }
     334              : 
     335              : /// The last row each attribute recorded `onChange` wrote, by (Tenant,
     336              : /// Entity, attribute, datasetId).
     337              : type LastRows = std::collections::HashMap<(String, String, String, String), LastRow>;
     338              : 
     339              : /// An attribute's newest row, and the last late row written below it.
     340              : struct LastRow {
     341              :     payload: Value,
     342              :     at: chrono::DateTime<chrono::Utc>,
     343              :     /// A late instance (older than `at`) is judged against the last late row,
     344              :     /// or against `at` before there is one, so a replayed or retried batch
     345              :     /// of older seconds records one row per heartbeat, not one per second
     346              :     /// (a Vehicle wrote one row in 4.5 s under a 30 s heartbeat).
     347              :     late: Option<(Value, chrono::DateTime<chrono::Utc>)>,
     348              : }
     349              : static LAST_ROWS: std::sync::LazyLock<std::sync::Mutex<LastRows>> =
     350              :     std::sync::LazyLock::new(Default::default);
     351              : /// The most attributes [`LAST_ROWS`] remembers.
     352              : // One map per process, cleared when full or restarted; the next
     353              : // write of each attribute then records a row, which costs a row, never one
     354              : // lost. A shared store if replicas ever record far more than one row per
     355              : // heartbeat between them.
     356              : const LAST_ROWS_MAX: usize = 500_000;
     357              : 
     358              : /// The Attributes of the temporal `doc` its type records `onChange`:
     359              : /// a query carries their level into a window and weighs
     360              : /// their values by the time each held.
     361         2054 : pub(crate) fn held_attrs(
     362         2054 :     st: &AppState,
     363         2054 :     tenant: &TenantId,
     364         2054 :     doc: &Value,
     365         2054 : ) -> std::collections::HashSet<String> {
     366         2054 :     let rules = st
     367         2054 :         .history_off
     368         2054 :         .read()
     369         2054 :         .unwrap_or_else(std::sync::PoisonError::into_inner);
     370         2054 :     let (false, Some(obj)) = (rules.recording.is_empty(), doc.as_object()) else {
     371         1902 :         return Default::default();
     372              :     };
     373          152 :     let types: Vec<&str> = match obj.get("type") {
     374            8 :         Some(Value::String(t)) => vec![t.as_str()],
     375          144 :         Some(Value::Array(ts)) => ts.iter().filter_map(Value::as_str).collect(),
     376            0 :         _ => Vec::new(),
     377              :     };
     378          152 :     let recordings: Vec<_> = types
     379          152 :         .iter()
     380          156 :         .filter_map(|t| {
     381          156 :             rules
     382          156 :                 .recording
     383          156 :                 .get(&(tenant.as_str().to_owned(), (*t).to_owned()))
     384          156 :         })
     385          152 :         .collect();
     386          152 :     obj.keys()
     387         1058 :         .filter(|k| {
     388         1058 :             recordings
     389         1058 :                 .iter()
     390         1058 :                 .any(|r| matches!(r.of(k), Some(RecordMode::OnChange(_))))
     391         1058 :         })
     392          152 :         .cloned()
     393          152 :         .collect()
     394         2054 : }
     395              : 
     396              : /// Does some type of `tenant` record an Attribute `onChange`? Its
     397              : /// aggregations are then computed here, never pushed into the store.
     398         1970 : pub(crate) fn records_on_change(st: &AppState, tenant: &TenantId) -> bool {
     399         1970 :     st.history_off
     400         1970 :         .read()
     401         1970 :         .unwrap_or_else(std::sync::PoisonError::into_inner)
     402         1970 :         .recording
     403         1970 :         .iter()
     404         1970 :         .any(|((t, _), r)| t == tenant.as_str() && r.has_on_change())
     405         1970 : }
     406              : 
     407              : /// Gate 4: whether an instance of `attr`, changed
     408              : /// on the Entity `entity` with `types` in `tenant`, writes a row. With no
     409              : /// rule the mode is `every`, or `observed` under `ruled`. `every`
     410              : /// always does, `never` never; `onChange` when its value moved past the
     411              : /// deadband or minimum distance since the attribute's last row, when it
     412              : /// differs at all from a row of the same `observedAt` (a correction), or
     413              : /// once the heartbeat passed. An instance older than that row (a backfill,
     414              : /// a retried batch) is judged the same way against the nearest row this
     415              : /// process knows below it.
     416              : /// `observed` when its `observedAt` is newer than that of the instance it
     417              : /// replaces (`before`, the attribute as the Entity held it).
     418       181882 : pub(crate) fn admits_change(
     419       181882 :     st: &AppState,
     420       181882 :     tenant: &TenantId,
     421       181882 :     types: &[&str],
     422       181882 :     entity: &str,
     423       181882 :     attr: &str,
     424       181882 :     before: Option<&Value>,
     425       181882 :     instance: &Value,
     426       181882 : ) -> bool {
     427              :     // An audited write keeps every change; gate 1 already dropped
     428              :     // an exact repeat.
     429       181882 :     if audited() {
     430            0 :         return true;
     431       181882 :     }
     432       181882 :     let default = if st.temporal_record == crate::state::TemporalRecord::Ruled {
     433           22 :         RecordMode::Observed
     434              :     } else {
     435       181860 :         RecordMode::Every
     436              :     };
     437       181882 :     let mode = {
     438       181882 :         let rules = st
     439       181882 :             .history_off
     440       181882 :             .read()
     441       181882 :             .unwrap_or_else(std::sync::PoisonError::into_inner);
     442       182690 :         types.iter().find_map(|t| {
     443       182690 :             rules
     444       182690 :                 .recording
     445       182690 :                 .get(&(tenant.as_str().to_owned(), (*t).to_owned()))
     446       182690 :                 .and_then(|r| r.of(attr))
     447       182690 :         })
     448              :     };
     449       181882 :     let mode = match mode.unwrap_or(default) {
     450       130050 :         RecordMode::Every => return true,
     451            4 :         RecordMode::Never => return false,
     452           46 :         RecordMode::Observed => return newly_observed(before, instance),
     453        51782 :         RecordMode::OnChange(mode) => mode,
     454              :     };
     455        51782 :     let at = observed_at(instance).unwrap_or_else(chrono::Utc::now);
     456        51782 :     let payload = payload_of(instance);
     457        51782 :     let key = (
     458        51782 :         tenant.as_str().to_owned(),
     459        51782 :         entity.to_owned(),
     460        51782 :         attr.to_owned(),
     461        51782 :         instance
     462        51782 :             .get("datasetId")
     463        51782 :             .and_then(Value::as_str)
     464        51782 :             .unwrap_or_default()
     465        51782 :             .to_owned(),
     466        51782 :     );
     467        51782 :     let mut last = LAST_ROWS
     468        51782 :         .lock()
     469        51782 :         .unwrap_or_else(std::sync::PoisonError::into_inner);
     470        51782 :     let admits = |was: &Value, when: chrono::DateTime<chrono::Utc>| {
     471        51744 :         if at == when {
     472           10 :             return payload != *was;
     473        51734 :         }
     474        51734 :         let apart = at.signed_duration_since(when).abs();
     475        51734 :         apart.to_std().is_ok_and(|d| d >= mode.heartbeat) || moved(&mode, was, &payload)
     476        51744 :     };
     477        51782 :     match last.get_mut(&key) {
     478        51744 :         Some(row) if at < row.at => {
     479          328 :             let (was, when) = row
     480          328 :                 .late
     481          328 :                 .as_ref()
     482          328 :                 .map_or((&row.payload, row.at), |(p, t)| (p, *t));
     483          328 :             let writes = admits(was, when);
     484          328 :             if writes {
     485           16 :                 row.late = Some((payload, at));
     486          312 :             }
     487          328 :             writes
     488              :         }
     489        51416 :         Some(row) if !admits(&row.payload, row.at) => false,
     490              :         _ => {
     491         1580 :             if last.len() >= LAST_ROWS_MAX {
     492            0 :                 last.clear();
     493         1580 :             }
     494         1580 :             last.insert(
     495         1580 :                 key,
     496         1580 :                 LastRow {
     497         1580 :                     payload,
     498         1580 :                     at,
     499         1580 :                     late: None,
     500         1580 :                 },
     501         1580 :             );
     502         1580 :             true
     503              :         }
     504              :     }
     505       181882 : }
     506              : 
     507              : /// The instant `instance` was observed at, if it says.
     508        51858 : fn observed_at(instance: &Value) -> Option<chrono::DateTime<chrono::Utc>> {
     509        51858 :     let s = instance.get("observedAt")?.as_str()?;
     510        51854 :     chrono::DateTime::parse_from_rfc3339(&antares_store::filter::canonical_datetime(s))
     511        51854 :         .ok()
     512        51854 :         .map(|t| t.with_timezone(&chrono::Utc))
     513        51858 : }
     514              : 
     515              : /// `record: observed`: whether `instance` carries an `observedAt` newer than
     516              : /// the instance of its datasetId in `before` (an array or one instance).
     517           46 : fn newly_observed(before: Option<&Value>, instance: &Value) -> bool {
     518           46 :     let Some(now) = observed_at(instance) else {
     519            4 :         return false;
     520              :     };
     521           68 :     let ds = |i: &Value| i.get("datasetId").cloned();
     522           42 :     let was = match before {
     523           30 :         Some(Value::Array(all)) => all.iter().find(|b| ds(b) == ds(instance)),
     524            4 :         Some(one) if ds(one) == ds(instance) => Some(one),
     525            8 :         _ => None,
     526              :     };
     527           42 :     was.and_then(observed_at).is_none_or(|was| now > was)
     528           46 : }
     529              : 
     530              : /// An instance without its stamps and provenance: what a row records.
     531        51782 : fn payload_of(instance: &Value) -> Value {
     532        51782 :     let mut p = instance.clone();
     533        51782 :     if let Some(o) = p.as_object_mut() {
     534       258910 :         for k in [
     535        51782 :             "instanceId",
     536        51782 :             "observedAt",
     537        51782 :             "createdAt",
     538        51782 :             "modifiedAt",
     539        51782 :             antares_store::PROVENANCE_KEY,
     540       258910 :         ] {
     541       258910 :             o.remove(k);
     542       258910 :         }
     543            0 :     }
     544        51782 :     p
     545        51782 : }
     546              : 
     547              : /// Whether `now` moved from `was` enough to be a row under `mode`.
     548        50710 : fn moved(mode: &crate::state::OnChange, was: &Value, now: &Value) -> bool {
     549       101420 :     let number = |v: &Value| v.get("value").and_then(Value::as_f64);
     550        50710 :     if let (Some(band), Some(a), Some(b)) = (mode.deadband, number(was), number(now)) {
     551          968 :         return (a - b).abs() > band;
     552        49742 :     }
     553        99484 :     let point = |v: &Value| {
     554        99484 :         let g = v.get("value")?;
     555        99484 :         let c = g.get("coordinates")?.as_array()?;
     556           16 :         (g.get("type")?.as_str()? == "Point").then_some((c.first()?.as_f64()?, c.get(1)?.as_f64()?))
     557        99484 :     };
     558        49742 :     if let (Some(metres), Some(a), Some(b)) = (mode.min_distance, point(was), point(now)) {
     559            8 :         return haversine(a, b) > metres;
     560        49734 :     }
     561        49734 :     was != now
     562        50710 : }
     563              : 
     564              : /// Metres between two (longitude, latitude) points on a sphere of the
     565              : /// Earth's mean radius: within a fraction of a percent at a city's scale.
     566            8 : fn haversine((lon1, lat1): (f64, f64), (lon2, lat2): (f64, f64)) -> f64 {
     567            8 :     let (p1, p2) = (lat1.to_radians(), lat2.to_radians());
     568            8 :     let (dp, dl) = ((lat2 - lat1).to_radians(), (lon2 - lon1).to_radians());
     569            8 :     let a = (dp / 2.0).sin().powi(2) + p1.cos() * p2.cos() * (dl / 2.0).sin().powi(2);
     570            8 :     2.0 * 6_371_008.8 * a.sqrt().asin()
     571            8 : }
     572              : 
     573              : /// The bounds a duration has to sit in: one minute to ten years.
     574              : const SHORTEST: Duration = Duration::from_secs(60);
     575              : const LONGEST: Duration = Duration::from_hours(87_600);
     576              : 
     577              : /// A duration as ISO 8601 (`PT5M`, `PT8H`, `P30D`, `P1DT12H`) or in short
     578              : /// form (`90s`, `5m`, `8h`, `14d`), between one minute and ten years.
     579              : ///
     580              : /// # Errors
     581              : /// Why `raw` is not one.
     582          100 : pub fn parse_duration(raw: &str) -> Result<Duration, String> {
     583          100 :     parse_duration_within(raw, SHORTEST, LONGEST)
     584          100 : }
     585              : 
     586              : /// A duration as [`parse_duration`] reads it, between `shortest` and
     587              : /// `longest` (the quality sweep's `maxAge` starts at one second).
     588              : ///
     589              : /// # Errors
     590              : /// Why `raw` is not one.
     591          100 : pub fn parse_duration_within(
     592          100 :     raw: &str,
     593          100 :     shortest: Duration,
     594          100 :     longest: Duration,
     595          100 : ) -> Result<Duration, String> {
     596          100 :     let bad = || format!("{raw:?} is not a duration such as PT5M, PT8H, P30D, 5m, 8h or 14d");
     597          100 :     let secs = if let Some(iso) = raw.strip_prefix('P') {
     598           60 :         let (days, time) = match iso.split_once('T') {
     599           36 :             Some((d, t)) if !t.is_empty() => (d, Some(t)),
     600            8 :             Some(_) => return Err(bad()),
     601           24 :             None => (iso, None),
     602              :         };
     603           52 :         let mut total = 0u64;
     604           52 :         if !days.is_empty() {
     605           24 :             total = days
     606           24 :                 .strip_suffix('D')
     607           24 :                 .and_then(|n| n.parse::<u64>().ok())
     608           24 :                 .ok_or_else(bad)?
     609           20 :                 .checked_mul(86_400)
     610           20 :                 .ok_or_else(bad)?;
     611           28 :         } else if time.is_none() {
     612            4 :             return Err(bad());
     613           24 :         }
     614           44 :         let mut rest = time.unwrap_or_default();
     615          128 :         for (unit, scale) in [('H', 3600u64), ('M', 60), ('S', 1)] {
     616          128 :             if let Some((n, tail)) = rest.split_once(unit) {
     617           32 :                 let n = n.parse::<u64>().map_err(|_| bad())?;
     618           28 :                 total = n
     619           28 :                     .checked_mul(scale)
     620           28 :                     .and_then(|n| total.checked_add(n))
     621           28 :                     .ok_or_else(bad)?;
     622           28 :                 rest = tail;
     623           96 :             }
     624              :         }
     625           40 :         if !rest.is_empty() {
     626            4 :             return Err(bad());
     627           36 :         }
     628           36 :         total
     629              :     } else {
     630           40 :         let split = raw.len().checked_sub(1).ok_or_else(bad)?;
     631           36 :         let (n, unit) = (
     632           36 :             raw.get(..split).ok_or_else(bad)?,
     633           36 :             raw.get(split..).ok_or_else(bad)?,
     634              :         );
     635           36 :         let scale = match unit {
     636           36 :             "s" => 1u64,
     637           32 :             "m" => 60,
     638           24 :             "h" => 3600,
     639           20 :             "d" => 86_400,
     640           16 :             _ => return Err(bad()),
     641              :         };
     642           20 :         n.parse::<u64>()
     643           20 :             .ok()
     644           20 :             .and_then(|n| n.checked_mul(scale))
     645           20 :             .ok_or_else(bad)?
     646              :     };
     647           52 :     let d = Duration::from_secs(secs);
     648           52 :     if d < shortest || d > longest {
     649            8 :         return Err(format!(
     650            8 :             "{raw:?} is outside {}s to {}s",
     651            8 :             shortest.as_secs(),
     652            8 :             longest.as_secs()
     653            8 :         ));
     654           44 :     }
     655           44 :     Ok(d)
     656          100 : }
     657              : 
     658              : /// The consumer side: one `event_list` call per drained batch.
     659       139660 : pub(crate) async fn drain(st: &AppState, evs: Vec<TemporalEvent>) {
     660       139660 :     let evs: Vec<TemporalEvent> = evs
     661       139660 :         .into_iter()
     662       140148 :         .filter(|ev| GATES.iter().all(|gate| gate(st, ev)))
     663       139660 :         .collect();
     664       139660 :     if evs.is_empty() {
     665        16176 :         return;
     666       123484 :     }
     667       123484 :     if let Err(e) = st.temporal.event_list(&evs).await {
     668            4 :         DRAIN_ERRORS.fetch_add(1, Ordering::Relaxed);
     669            4 :         metrics::counter!("antares_temporal_drain_errors_total").increment(1);
     670            4 :         tracing::warn!(events = evs.len(), "temporal drain failed: {e}");
     671       123480 :     }
     672       139660 : }
     673              : 
     674              : /// What one request buffered: its history events and its changes for the
     675              : /// matcher. They stand for writes that already committed, so they are
     676              : /// handed on however the request ends — a handler that panicked, or a
     677              : /// request whose future was dropped (a client that went away) still leaves
     678              : /// its history and its notifications, which dropping the buffers with the
     679              : /// task-locals lost.
     680              : #[cfg(not(target_arch = "wasm32"))]
     681              : struct Buffered {
     682              :     st: AppState,
     683              :     evs: Vec<TemporalEvent>,
     684              :     changes: Vec<crate::mirror::Change>,
     685              : }
     686              : 
     687              : #[cfg(not(target_arch = "wasm32"))]
     688              : impl Buffered {
     689              :     /// Takes what the request's buffers hold; called inside their scope.
     690       110068 :     fn collect(&mut self) {
     691       110068 :         if let Ok(evs) = BUFFER.try_with(std::cell::RefCell::take) {
     692        55038 :             self.evs.extend(evs);
     693        55038 :         }
     694       110068 :         if let Ok(changes) = CHANGES.try_with(std::cell::RefCell::take) {
     695        55038 :             self.changes.extend(changes);
     696        55038 :         }
     697       110068 :     }
     698              : 
     699              :     /// The drain and the hand-over to the matcher as a task of their own:
     700              :     /// once started they finish whether or not anybody still waits for them,
     701              :     /// so a request dropped in the middle of the drain loses neither half.
     702              :     /// `None` when there is nothing to hand on.
     703       110068 :     fn hand_on(&mut self) -> Option<tokio::task::JoinHandle<()>> {
     704       110068 :         if self.evs.is_empty() && self.changes.is_empty() {
     705        82802 :             return None;
     706        27266 :         }
     707        27266 :         let Ok(rt) = tokio::runtime::Handle::try_current() else {
     708            0 :             tracing::warn!(
     709            0 :                 events = self.evs.len(),
     710            0 :                 changes = self.changes.len(),
     711              :                 "no runtime left to record a request's history"
     712              :             );
     713            0 :             return None;
     714              :         };
     715        27266 :         let (st, evs, changes) = (
     716        27266 :             self.st.clone(),
     717        27266 :             std::mem::take(&mut self.evs),
     718        27266 :             std::mem::take(&mut self.changes),
     719        27266 :         );
     720        27266 :         Some(rt.spawn(async move {
     721        27266 :             drain(&st, evs).await;
     722        27266 :             if !changes.is_empty() {
     723        27264 :                 if let Some(flush) = &st.change_flush {
     724        27264 :                     flush(changes);
     725        27264 :                 }
     726            2 :             }
     727        27266 :         }))
     728       110068 :     }
     729              : }
     730              : 
     731              : /// Dropped before [`layer`] took it out of the buffers' scope: the request
     732              : /// ended without an answer. Tokio drops a scoped future with its
     733              : /// task-locals set, so the buffers are still there to collect.
     734              : #[cfg(not(target_arch = "wasm32"))]
     735              : impl Drop for Buffered {
     736        55038 :     fn drop(&mut self) {
     737        55038 :         self.collect();
     738        55038 :         let _ = self.hand_on();
     739        55038 :     }
     740              : }
     741              : 
     742              : /// Router layer: scopes the buffer over the handler and drains it once the
     743              : /// response is built. Drained BEFORE the response is returned so
     744              : /// read-your-writes holds at any store latency (the ETSI temporal suites
     745              : /// read history straight after the write).
     746              : #[cfg(not(target_arch = "wasm32"))]
     747        55038 : pub(crate) async fn layer(
     748        55038 :     axum::extract::State(st): axum::extract::State<AppState>,
     749        55038 :     req: axum::extract::Request,
     750        55038 :     next: axum::middleware::Next,
     751        55038 : ) -> axum::response::Response {
     752        55038 :     let provenance = crate::provenance::of_request(req.headers(), st.provenance_from.as_deref());
     753        55038 :     let tenant = crate::negotiate::tenant_from(req.headers()).ok();
     754        55038 :     let (resp, mut buffered) = BUFFER
     755        55038 :         .scope(std::cell::RefCell::new(Vec::new()), async {
     756        55038 :             STAGED
     757        55038 :                 .scope(
     758        55038 :                     std::cell::RefCell::new(Vec::new()),
     759        55038 :                     CHANGES.scope(std::cell::RefCell::new(Vec::new()), async {
     760        55038 :                         PROVENANCE
     761        55038 :                             .scope(
     762        55038 :                                 provenance,
     763        55038 :                                 TENANT.scope(tenant, async {
     764        55038 :                                     let mut buffered = Buffered {
     765        55038 :                                         st: st.clone(),
     766        55038 :                                         evs: Vec::new(),
     767        55038 :                                         changes: Vec::new(),
     768        55038 :                                     };
     769        55038 :                                     let resp = next.run(req).await;
     770        55034 :                                     buffered.collect();
     771        55034 :                                     (resp, buffered)
     772        55034 :                                 }),
     773              :                             )
     774        55038 :                             .await
     775        55034 :                     }),
     776              :                 )
     777        55038 :                 .await
     778        55034 :         })
     779        55038 :         .await;
     780        55030 :     if let Some(task) = buffered.hand_on() {
     781        27258 :         if let Err(e) = task.await {
     782            0 :             tracing::warn!("recording a request's history failed: {e}");
     783        27258 :         }
     784        27772 :     }
     785        55030 :     resp
     786        55030 : }
     787              : 
     788              : /// No buffer on wasm (pushes drain on the spot): the layer is a pass-through
     789              : /// so the router composes identically on both targets.
     790              : #[cfg(target_arch = "wasm32")]
     791              : pub(crate) async fn layer(
     792              :     axum::extract::State(_st): axum::extract::State<AppState>,
     793              :     req: axum::extract::Request,
     794              :     next: axum::middleware::Next,
     795              : ) -> axum::response::Response {
     796              :     next.run(req).await
     797              : }
     798              : 
     799              : /// delete_temporal_on_core_delete: entity deletion removes its temporal
     800              : /// representation too (suite configuration parity). Skipped on bus=nats
     801              : /// api pods — the recorder applies the entityDeleted fence instead.
     802        11424 : pub(crate) async fn mirror_delete_entity(st: &AppState, tenant: &TenantId, id: &str) {
     803        11424 :     if !st.record_locally() {
     804            0 :         return;
     805        11424 :     }
     806        11424 :     if let Err(e) = st.temporal.delete(tenant, id).await {
     807            4 :         note_mirror_error();
     808            4 :         tracing::warn!("temporal mirror delete failed: {e}");
     809        11420 :     }
     810        11424 : }
     811              : 
     812              : /// A history write of the delete mirror failed: counted with the drain's,
     813              : /// because the history it should have recorded is lost the same way.
     814            8 : fn note_mirror_error() {
     815            8 :     DRAIN_ERRORS.fetch_add(1, Ordering::Relaxed);
     816            8 :     metrics::counter!("antares_temporal_drain_errors_total").increment(1);
     817            8 : }
     818              : 
     819              : /// 4.5.7/4.5.8: "In case the Property is deleted, an instance of the
     820              : /// Property is recorded with its value set to the URI "urn:ngsi-ld:null"
     821              : /// and the deletedAt Temporal Property set" (object for a Relationship;
     822              : /// typed null shapes for the LanguageProperty/JsonProperty/Vocab/List
     823              : /// subtypes). Each recorded instance carries an instanceId — the clause
     824              : /// SHOULD that makes 5.6.14/5.6.15 selective modification possible.
     825          464 : pub(crate) async fn mirror_delete_attr(
     826          464 :     st: &AppState,
     827          464 :     tenant: &TenantId,
     828          464 :     id: &str,
     829          464 :     attr_iri: &str,
     830          464 :     dataset_id: Option<&str>,
     831          464 :     ts: &str,
     832          464 : ) {
     833          464 :     let r = st
     834          464 :         .temporal
     835          464 :         .mutate(tenant, id, |doc| {
     836              :             // The mirror writes nothing into a document the temporal driver
     837              :             // handed back in a shape the contract forbids.
     838          452 :             let Some(target) = doc.as_object_mut() else {
     839            0 :                 return Ok::<(), std::convert::Infallible>(());
     840              :             };
     841          452 :             if attr_iri == "scope" {
     842              :                 // scope deletion: a deleted scope is the NGSI-LD Null (4.18:
     843              :                 // urn:ngsi-ld:null appears only for deleted scopes; 4.5.0)
     844           24 :                 let inst = serde_json::json!({
     845           24 :                     "type": "Property",
     846           24 :                     "value": "urn:ngsi-ld:null",
     847           24 :                     "instanceId": format!("urn:ngsi-ld:Instance:{}", uuid::Uuid::new_v4()),
     848           24 :                     "deletedAt": ts, "createdAt": ts, "modifiedAt": ts,
     849              :                 });
     850           24 :                 match target.get_mut("scope").and_then(Value::as_array_mut) {
     851           24 :                     Some(arr) if arr.first().is_some_and(|i| i.is_object()) => arr.push(inst),
     852           24 :                     _ => {
     853           24 :                         target.insert("scope".into(), Value::Array(vec![inst]));
     854           24 :                     }
     855              :                 }
     856           24 :                 return Ok::<(), std::convert::Infallible>(());
     857          428 :             }
     858          428 :             if let Some(arr) = target.get_mut(attr_iri).and_then(Value::as_array_mut) {
     859          428 :                 if arr.is_empty() {
     860            0 :                     return Ok(());
     861          428 :                 }
     862          428 :                 let atype = arr
     863          428 :                     .first()
     864          428 :                     .and_then(|i| i.get("type"))
     865          428 :                     .and_then(Value::as_str)
     866          428 :                     .unwrap_or("Property")
     867          428 :                     .to_owned();
     868          428 :                 let mut inst = Map::new();
     869          428 :                 inst.insert("type".into(), Value::String(atype.clone()));
     870          428 :                 let null = Value::String("urn:ngsi-ld:null".into());
     871          428 :                 match atype.as_str() {
     872          428 :                     "Relationship" => {
     873           84 :                         inst.insert("object".into(), null);
     874           84 :                     }
     875          344 :                     "LanguageProperty" => {
     876           60 :                         inst.insert(
     877           60 :                             "languageMap".into(),
     878           60 :                             serde_json::json!({"@none": "urn:ngsi-ld:null"}),
     879           60 :                         );
     880           60 :                     }
     881          284 :                     "JsonProperty" => {
     882            4 :                         inst.insert("json".into(), null);
     883            4 :                     }
     884          280 :                     "VocabProperty" => {
     885            8 :                         inst.insert("vocab".into(), null);
     886            8 :                     }
     887          272 :                     "ListProperty" => {
     888            0 :                         inst.insert("valueList".into(), null);
     889            0 :                     }
     890          272 :                     "ListRelationship" => {
     891            0 :                         inst.insert("objectList".into(), null);
     892            0 :                     }
     893          272 :                     _ => {
     894          272 :                         inst.insert("value".into(), null);
     895          272 :                     }
     896              :                 }
     897          428 :                 if let Some(ds) = dataset_id {
     898           84 :                     inst.insert("datasetId".into(), Value::String(ds.to_owned()));
     899          344 :                 }
     900          428 :                 inst.insert(
     901          428 :                     "instanceId".into(),
     902          428 :                     Value::String(format!("urn:ngsi-ld:Instance:{}", uuid::Uuid::new_v4())),
     903              :                 );
     904          428 :                 inst.insert("deletedAt".into(), Value::String(ts.to_owned()));
     905              :                 // the tombstone instance is written at the deletion: a
     906              :                 // timeproperty=createdAt|modifiedAt window finds it there
     907          428 :                 inst.insert("createdAt".into(), Value::String(ts.to_owned()));
     908          428 :                 inst.insert("modifiedAt".into(), Value::String(ts.to_owned()));
     909          428 :                 inst.insert(antares_store::PROVENANCE_KEY.into(), provenance("delete"));
     910          428 :                 arr.push(Value::Object(inst));
     911            0 :             }
     912          428 :             Ok(())
     913          452 :         })
     914          464 :         .await;
     915          464 :     if let Err(e) = r {
     916            4 :         note_mirror_error();
     917            4 :         tracing::warn!("temporal attr mirror failed: {e}");
     918          460 :     }
     919          464 : }
     920              : 
     921              : #[cfg(all(test, not(target_arch = "wasm32")))]
     922              : mod tests {
     923              :     use super::*;
     924              : 
     925              :     /// A history store whose writes all fail.
     926              :     struct Down;
     927              : 
     928              :     #[async_trait::async_trait]
     929              :     impl antares_store::TemporalDriver for Down {
     930            0 :         async fn event_list(&self, _evs: &[TemporalEvent]) -> Result<(), antares_model::NgsiError> {
     931              :             Err(antares_model::NgsiError::InternalError(
     932              :                 "history store down".into(),
     933              :             ))
     934            0 :         }
     935              :         async fn temporal_append(
     936              :             &self,
     937              :             _t: &TenantId,
     938              :             _id: &str,
     939              :             _shell: &Value,
     940              :             _additions: &Value,
     941            0 :         ) -> Result<(), antares_model::NgsiError> {
     942              :             Err(antares_model::NgsiError::InternalError(
     943              :                 "history store down".into(),
     944              :             ))
     945            0 :         }
     946              :         async fn query_temporal(
     947              :             &self,
     948              :             _t: &TenantId,
     949              :             _f: &antares_store::filter::TemporalFilter<'_>,
     950            0 :         ) -> Result<antares_store::filter::TemporalOutcome, antares_model::NgsiError> {
     951              :             Err(antares_model::NgsiError::InternalError(
     952              :                 "history store down".into(),
     953              :             ))
     954            0 :         }
     955              :         async fn get_temporal(
     956              :             &self,
     957              :             _t: &TenantId,
     958              :             _id: &str,
     959              :             _f: &antares_store::filter::TemporalFilter<'_>,
     960            0 :         ) -> Result<Option<Value>, antares_model::NgsiError> {
     961              :             Err(antares_model::NgsiError::InternalError(
     962              :                 "history store down".into(),
     963              :             ))
     964            0 :         }
     965              :         async fn get(
     966              :             &self,
     967              :             _t: &TenantId,
     968              :             _id: &str,
     969            0 :         ) -> Result<Option<Value>, antares_model::NgsiError> {
     970              :             Err(antares_model::NgsiError::InternalError(
     971              :                 "history store down".into(),
     972              :             ))
     973            0 :         }
     974              :         async fn create(
     975              :             &self,
     976              :             _t: &TenantId,
     977              :             _id: &str,
     978              :             _doc: Value,
     979            0 :         ) -> Result<bool, antares_model::NgsiError> {
     980              :             Err(antares_model::NgsiError::InternalError(
     981              :                 "history store down".into(),
     982              :             ))
     983            0 :         }
     984              :         async fn upsert(
     985              :             &self,
     986              :             _t: &TenantId,
     987              :             _id: &str,
     988              :             _doc: Value,
     989            0 :         ) -> Result<bool, antares_model::NgsiError> {
     990              :             Err(antares_model::NgsiError::InternalError(
     991              :                 "history store down".into(),
     992              :             ))
     993            0 :         }
     994            4 :         async fn delete(&self, _t: &TenantId, _id: &str) -> Result<bool, antares_model::NgsiError> {
     995              :             Err(antares_model::NgsiError::InternalError(
     996              :                 "history store down".into(),
     997              :             ))
     998            4 :         }
     999            0 :         async fn list(&self, _t: &TenantId) -> Result<Vec<Value>, antares_model::NgsiError> {
    1000              :             Err(antares_model::NgsiError::InternalError(
    1001              :                 "history store down".into(),
    1002              :             ))
    1003            0 :         }
    1004              :         async fn mutate_boxed<'a>(
    1005              :             &self,
    1006              :             _t: &TenantId,
    1007              :             _id: &str,
    1008              :             _f: antares_store::MutateFn<'a>,
    1009            4 :         ) -> Result<Option<Result<(), ()>>, antares_model::NgsiError> {
    1010              :             Err(antares_model::NgsiError::InternalError(
    1011              :                 "history store down".into(),
    1012              :             ))
    1013            4 :         }
    1014              :     }
    1015              : 
    1016              :     /// The delete mirrors lose history exactly as a failed drain does, so
    1017              :     /// they are counted in the same `temporalDrainErrors`.
    1018              :     #[tokio::test]
    1019            4 :     async fn a_failed_delete_mirror_is_counted_as_a_drain_error() {
    1020            4 :         let base = AppState::new("antares-mirror-errors".into());
    1021            4 :         let st = AppState::with_drivers(
    1022            4 :             "antares-mirror-errors".into(),
    1023            4 :             base.store.clone(),
    1024            4 :             std::sync::Arc::new(Down),
    1025            4 :             "memory",
    1026              :         );
    1027            4 :         let t = TenantId::new("mirrorerrors").expect("tenant");
    1028            4 :         let before = drain_errors();
    1029            4 :         mirror_delete_entity(&st, &t, "urn:ngsi-ld:T:1").await;
    1030            4 :         let after_entity = drain_errors();
    1031            4 :         assert!(
    1032            4 :             after_entity > before,
    1033              :             "the entity delete mirror was not counted"
    1034              :         );
    1035            4 :         mirror_delete_attr(
    1036            4 :             &st,
    1037            4 :             &t,
    1038            4 :             "urn:ngsi-ld:T:1",
    1039            4 :             "https://x/a",
    1040            4 :             None,
    1041            4 :             "2026-01-01T00:00:00Z",
    1042            4 :         )
    1043            4 :         .await;
    1044            4 :         assert!(
    1045            4 :             drain_errors() > after_entity,
    1046            4 :             "the attribute delete mirror was not counted"
    1047            4 :         );
    1048            4 :     }
    1049              : 
    1050           24 :     fn ev(id: &str) -> TemporalEvent {
    1051           24 :         TemporalEvent {
    1052           24 :             op: antares_store::TemporalOp::AttrModified,
    1053           24 :             tenant: TenantId::new("t").expect("tenant"),
    1054           24 :             entity_id: id.to_owned(),
    1055           24 :             shell: serde_json::json!({"id": id}),
    1056           24 :             attr: "https://example.org/level".to_owned(),
    1057           24 :             instance: serde_json::json!({"type": "Property", "value": 1}),
    1058           24 :         }
    1059           24 :     }
    1060              : 
    1061              :     /// a backfill found inside a read-modify-write reaches the
    1062              :     /// request's history only when that write commits; a refused write
    1063              :     /// leaves none, and a rerun closure records its backfill once.
    1064              :     #[tokio::test]
    1065            4 :     async fn a_backfill_is_recorded_only_for_a_committed_write() {
    1066            4 :         let buffered = BUFFER
    1067            4 :             .scope(std::cell::RefCell::new(Vec::new()), async {
    1068            4 :                 STAGED
    1069            4 :                     .scope(std::cell::RefCell::new(Vec::new()), async {
    1070              :                         // refused: nothing reaches history
    1071            4 :                         restage("urn:a");
    1072            4 :                         assert!(stage(ev("urn:a")));
    1073            4 :                         settle("urn:a", false);
    1074            4 :                         assert_eq!(BUFFER.with(|b| b.borrow().len()), 0);
    1075              :                         // rerun twice, then committed: one event
    1076            4 :                         restage("urn:b");
    1077            4 :                         assert!(stage(ev("urn:b")));
    1078            4 :                         restage("urn:b");
    1079            4 :                         assert!(stage(ev("urn:b")));
    1080              :                         // another Entity's staged event stays staged
    1081            4 :                         assert!(stage(ev("urn:c")));
    1082            4 :                         settle_all::<(), ()>(&["urn:b".to_owned()], &[Some(Ok(()))]);
    1083            4 :                         assert_eq!(STAGED.with(|b| b.borrow().len()), 1);
    1084            4 :                         BUFFER.with(|b| b.take())
    1085            4 :                     })
    1086            4 :                     .await
    1087            4 :             })
    1088            4 :             .await;
    1089            4 :         let ids: Vec<&str> = buffered.iter().map(|e| e.entity_id.as_str()).collect();
    1090            4 :         assert_eq!(ids, ["urn:b"]);
    1091            4 :     }
    1092              : 
    1093              :     /// A handler behind [`layer`] that commits a write, buffers its history
    1094              :     /// event and its change, and then ends as `end` does; the matcher's
    1095              :     /// hand-over is counted.
    1096            8 :     fn committed_then(
    1097            8 :         end: fn() -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
    1098            8 :     ) -> (AppState, axum::Router, std::sync::Arc<AtomicU64>) {
    1099            8 :         let mut st = AppState::new("t".into());
    1100            8 :         let flushed = std::sync::Arc::new(AtomicU64::new(0));
    1101            8 :         let counter = flushed.clone();
    1102            8 :         st.change_flush = Some(std::sync::Arc::new(move |changes| {
    1103            8 :             counter.fetch_add(changes.len() as u64, Ordering::SeqCst);
    1104            8 :         }));
    1105            8 :         let handler = move |axum::extract::State(st): axum::extract::State<AppState>| async move {
    1106            8 :             let tenant = TenantId::new("t").expect("tenant");
    1107            8 :             let doc =
    1108            8 :                 serde_json::json!({"id": "urn:ngsi-ld:T:1", "type": ["https://example.org/T"]});
    1109            8 :             st.store
    1110            8 :                 .upsert(&tenant, antares_store::Kind::Entity, "urn:ngsi-ld:T:1", doc)
    1111            8 :                 .await
    1112            8 :                 .expect("commit");
    1113            8 :             push(&st, ev("urn:ngsi-ld:T:1")).await;
    1114            8 :             assert!(buffer_change(("urn:ngsi-ld:T:1".to_owned(), None, None)).is_none());
    1115            8 :             end().await;
    1116           12 :         };
    1117            8 :         let app = axum::Router::new()
    1118            8 :             .route("/w", axum::routing::post(handler))
    1119            8 :             .layer(axum::middleware::from_fn_with_state(st.clone(), layer))
    1120            8 :             .layer(tower_http::catch_panic::CatchPanicLayer::new())
    1121            8 :             .with_state(st.clone());
    1122            8 :         (st, app, flushed)
    1123            8 :     }
    1124              : 
    1125              :     /// Whether the event `committed_then` buffered reached history and its
    1126              :     /// change the matcher, waiting a moment for the task that hands them on.
    1127            8 :     async fn handed_on(st: &AppState, flushed: &AtomicU64) -> bool {
    1128            8 :         let tenant = TenantId::new("t").expect("tenant");
    1129            8 :         for _ in 0..200 {
    1130           16 :             let recorded = st
    1131           16 :                 .temporal
    1132           16 :                 .get(&tenant, "urn:ngsi-ld:T:1")
    1133           16 :                 .await
    1134           16 :                 .expect("read")
    1135           16 :                 .is_some();
    1136           16 :             if recorded && flushed.load(Ordering::SeqCst) == 1 {
    1137            8 :                 return true;
    1138            8 :             }
    1139            8 :             tokio::time::sleep(Duration::from_millis(10)).await;
    1140              :         }
    1141            0 :         false
    1142            8 :     }
    1143              : 
    1144            8 :     fn post() -> axum::extract::Request {
    1145            8 :         axum::http::Request::post("/w")
    1146            8 :             .body(axum::body::Body::empty())
    1147            8 :             .expect("request")
    1148            8 :     }
    1149              : 
    1150              :     /// A client that goes away drops its request's future: what the
    1151              :     /// handler already committed is still recorded and still notified.
    1152              :     #[tokio::test]
    1153            4 :     async fn a_dropped_request_still_records_and_notifies_what_it_committed() {
    1154              :         use tower::ServiceExt as _;
    1155            4 :         let (st, app, flushed) = committed_then(|| Box::pin(std::future::pending()));
    1156            4 :         let answer = tokio::time::timeout(Duration::from_millis(200), app.oneshot(post())).await;
    1157            4 :         assert!(answer.is_err(), "the handler never answers");
    1158            4 :         assert!(
    1159            4 :             handed_on(&st, &flushed).await,
    1160            4 :             "the committed write was lost"
    1161            4 :         );
    1162            4 :     }
    1163              : 
    1164              :     /// So is a handler that panics after its write committed (the 500
    1165              :     /// comes from a layer outside this one).
    1166              :     #[tokio::test]
    1167            4 :     async fn a_panicking_handler_still_records_and_notifies_what_it_committed() {
    1168              :         use tower::ServiceExt as _;
    1169            4 :         let (st, app, flushed) = committed_then(|| Box::pin(async { panic!("after the commit") }));
    1170            4 :         let resp = app.oneshot(post()).await.expect("response");
    1171            4 :         assert_eq!(resp.status(), axum::http::StatusCode::INTERNAL_SERVER_ERROR);
    1172            4 :         assert!(
    1173            4 :             handed_on(&st, &flushed).await,
    1174            4 :             "the committed write was lost"
    1175            4 :         );
    1176            4 :     }
    1177              : }
        

Generated by: LCOV version 2.0-1