Part 11 modified-value history (#668) follow-ups: QuestDB TTL exposure, schema-v2 posture, cross-node ordering #674

Closed
opened 2026-10-11 10:18:14 -04:00 by dohertj2 · 1 comment
Owner

Follow-ups from the #668 review and live gate (Part 11 modified-value history).

  1. QuestDB sample_modified TTL vs far-future timestamps. The TTL is keyed on the sample's source time. If QuestDB measures TTL from the table's max timestamp (verify for the deployed version), one HistoryUpdate Insert at a far-future time (e.g. a 2062 typo) drops every older audit partition on the next commit — the record of who changed history is erased by a history change. sample has the same exposure. Consider rejecting far-future HistoryUpdate timestamps.
  2. RequiredVersion = 2 stops all historian traffic (incl. continuous-historization drains) on AutoCreateSchema=false until script 002 is applied. Alternative: accept v1 and fail only modified reads / HistoryUpdate.
  3. QuestDB cross-node: the per-tag lock is per process, so concurrent updates from both pair nodes can record a wrong prior; a trail INSERT retried after a client timeout can duplicate rows (no DEDUP); ModifiedId ordering follows each node's wall clock.
  4. Posture check: with Security:Acl:Enabled=false any admitted session (incl. anonymous) can read who rewrote history (documented in security.md).
  5. Not implemented: IsDeleteModified; upsert recorded as Update (not split Insert/Replace); no AuditHistoryUpdateEvent.
  6. Client.CLI historyread --modified prints a stack trace on a Bad node status (HistoryReadStatus.ThrowIfRefused) — print the status cleanly.
Follow-ups from the #668 review and live gate (Part 11 modified-value history). 1. **QuestDB `sample_modified` TTL vs far-future timestamps.** The TTL is keyed on the sample's source time. If QuestDB measures TTL from the table's max timestamp (verify for the deployed version), one HistoryUpdate Insert at a far-future time (e.g. a 2062 typo) drops every older audit partition on the next commit — the record of who changed history is erased by a history change. `sample` has the same exposure. Consider rejecting far-future HistoryUpdate timestamps. 2. **`RequiredVersion = 2` stops all historian traffic** (incl. continuous-historization drains) on `AutoCreateSchema=false` until script 002 is applied. Alternative: accept v1 and fail only modified reads / HistoryUpdate. 3. **QuestDB cross-node:** the per-tag lock is per process, so concurrent updates from both pair nodes can record a wrong prior; a trail INSERT retried after a client timeout can duplicate rows (no DEDUP); `ModifiedId` ordering follows each node's wall clock. 4. **Posture check:** with `Security:Acl:Enabled=false` any admitted session (incl. anonymous) can read who rewrote history (documented in security.md). 5. Not implemented: `IsDeleteModified`; upsert recorded as `Update` (not split Insert/Replace); no `AuditHistoryUpdateEvent`. 6. Client.CLI `historyread --modified` prints a stack trace on a Bad node status (`HistoryReadStatus.ThrowIfRefused`) — print the status cleanly.
Author
Owner

Done — merged to master (e59ab693, review follow-ups 029a9162; client item in 3067146b/045a21da).

  1. QuestDB TTL vs far-future timestamps — not a threat at the default config. Spike on questdb/questdb:10.0.1: TTL is judged against min(max table timestamp, wall clock) (cairo.ttl.use.wall.clock=true, the default); a 2066 row evicted nothing. Pinned by TtlWallClockTests (a partition >30 days old IS evicted, partitions inside TTL survive a far-future commit) + a docs note to keep the setting true. As hygiene, HistoryUpdate now refuses a SourceTimestamp beyond now + ServerHistorian:HistoryUpdateMaxFutureSkew (default 1 day; ≤0 disables) with per-item BadOutOfRange — all backends; the live recorder is not bounded (device clocks may run ahead).
  2. SqlServer v1 accepted. Base schema requirement back to v1; SupportsModifiedTrail (v ≥ 2). On v1 historization/raw/events/retention keep working; modified reads and HistoryUpdate refuse node-level BadHistoryOperationUnsupported with nothing sent; retention skips SampleModified; one Warning per boot. (A peer upgrading the DB to v2 at runtime is picked up on restart.)
  3. Cross-node: HistoryUpdate is now Primary-only — off-Primary ⇒ BadInvalidState; closed until this node's first redundancy snapshot (boot window), admin-only nodes ungated. Unsupported requests answer BadHistoryOperationUnsupported before the gate. QuestDB retry duplicates documented (no DEDUP key can collapse them).
    4./5. Documented (anonymous-readable with ACL off; IsDeleteModified / split Insert-Replace / AuditHistoryUpdateEvent not implemented).
  4. Client.CLI history refusal prints one line, no stack trace.

Live gate (docker-dev, livegate-5): Primary (central-2, ServiceLevel 250) insert ⇒ GoodEntryInserted, Secondary ⇒ BadInvalidState and no row; now+2d ⇒ BadOutOfRange, now+12h accepted. v1-only DB with AutoCreateSchema=false: hist.Sample kept rising, raw reads OK, --modified/HistoryUpdate ⇒ BadHistoryOperationUnsupported, one Warning per node.

Done — merged to master (`e59ab693`, review follow-ups `029a9162`; client item in `3067146b`/`045a21da`). 1. **QuestDB TTL vs far-future timestamps — not a threat at the default config.** Spike on `questdb/questdb:10.0.1`: TTL is judged against `min(max table timestamp, wall clock)` (`cairo.ttl.use.wall.clock=true`, the default); a 2066 row evicted nothing. Pinned by `TtlWallClockTests` (a partition >30 days old IS evicted, partitions inside TTL survive a far-future commit) + a docs note to keep the setting `true`. As hygiene, HistoryUpdate now refuses a SourceTimestamp beyond `now + ServerHistorian:HistoryUpdateMaxFutureSkew` (default 1 day; ≤0 disables) with per-item `BadOutOfRange` — all backends; the live recorder is not bounded (device clocks may run ahead). 2. **SqlServer v1 accepted.** Base schema requirement back to v1; `SupportsModifiedTrail` (v ≥ 2). On v1 historization/raw/events/retention keep working; modified reads and HistoryUpdate refuse node-level `BadHistoryOperationUnsupported` with nothing sent; retention skips `SampleModified`; one Warning per boot. (A peer upgrading the DB to v2 at runtime is picked up on restart.) 3. **Cross-node:** HistoryUpdate is now **Primary-only** — off-Primary ⇒ `BadInvalidState`; closed until this node's first redundancy snapshot (boot window), admin-only nodes ungated. Unsupported requests answer `BadHistoryOperationUnsupported` before the gate. QuestDB retry duplicates documented (no DEDUP key can collapse them). 4./5. Documented (anonymous-readable with ACL off; IsDeleteModified / split Insert-Replace / AuditHistoryUpdateEvent not implemented). 6. Client.CLI history refusal prints one line, no stack trace. **Live gate (docker-dev, livegate-5):** Primary (central-2, ServiceLevel 250) insert ⇒ GoodEntryInserted, Secondary ⇒ `BadInvalidState` and no row; now+2d ⇒ `BadOutOfRange`, now+12h accepted. v1-only DB with `AutoCreateSchema=false`: `hist.Sample` kept rising, raw reads OK, `--modified`/HistoryUpdate ⇒ `BadHistoryOperationUnsupported`, one Warning per node.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#674