chore(historian): batch-era defaults + appsettings/doc alignment
This commit is contained in:
@@ -383,6 +383,24 @@ key must carry the scopes `historian:read`, `historian:write`, `historian:tags:w
|
||||
[`docs/Historian.md`](Historian.md) for the full key reference, the migration note (old Wonderware keys →
|
||||
gateway keys), and the deployment prerequisites.
|
||||
|
||||
#### `ContinuousHistorization` (recorder + outbox knobs)
|
||||
|
||||
Only the knobs; the gateway connection comes from `ServerHistorian`. Full descriptions in
|
||||
[`docs/Historian.md` §ContinuousHistorization configuration](Historian.md#continuoushistorization-configuration).
|
||||
|
||||
| Key | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `ContinuousHistorization:Enabled` | `false` | `true` **and** a configured `ServerHistorian` wires the FasterLog outbox and spawns the recorder. |
|
||||
| `ContinuousHistorization:OutboxPath` | `""` | **Directory** for the FasterLog segment + commit files. Required when enabled; use an **absolute** path on durable storage. |
|
||||
| `ContinuousHistorization:CommitMode` | `PerEntry` | `PerEntry` fsyncs before the append returns; `Periodic` commits on a timer every `CommitIntervalMs`. **`PerEntry` is one fsync per ingest *burst*, not per value** — the recorder appends a burst through `AppendBatchAsync`, which enqueues every entry and issues a single group commit. Durability is unchanged: the call does not return until that commit lands. |
|
||||
| `ContinuousHistorization:CommitIntervalMs` | `100` | Periodic-mode cadence (ms); must be `> 0` under `Periodic`. |
|
||||
| `ContinuousHistorization:DrainBatchSize` | `1024` | Entries peeked + written per drain pass. **Raised from 64**: the peek is an in-memory scan (widening it costs allocation, not I/O) and the pass is now bounded-parallel rather than sequential, so a wide batch no longer costs `tags × latency`. 64 drew down ≤ 64 entries per `DrainIntervalSeconds`, so any gateway hiccup left the outbox permanently behind, growing toward `Capacity` and shedding oldest samples. |
|
||||
| `ContinuousHistorization:DrainIntervalSeconds` | `2` | Steady drain cadence (and post-success reschedule). |
|
||||
| `ContinuousHistorization:DrainConcurrency` | `8` | Historian tags one drain pass writes **concurrently** — a batch is grouped per tag and each group is its own gateway round-trip, so this turns a sequential `tags × latency` pass into `⌈tags / concurrency⌉ × latency` while bounding outstanding round-trips. `1` = fully sequential; `< 1` is clamped to 1 with a `Validate()` warning. The ack is unchanged (all-or-nothing after every group reports), as is the #538 quarantine rule. |
|
||||
| `ContinuousHistorization:Capacity` | `500000` | Max un-acked entries before drop-oldest; `0` = unbounded. The #538 disk backstop for a historian refusing **every** write — the one case the per-tag quarantine cannot resolve. |
|
||||
| `ContinuousHistorization:MaxWriteAttempts` | `5` | #538 poison-tag budget: consecutive failed passes one tag may accumulate — **counted only across passes where *other* tags wrote successfully** — before its entries are abandoned so the FIFO head can advance. |
|
||||
| `ContinuousHistorization:MinBackoffSeconds` / `MaxBackoffSeconds` | `1` / `30` | Initial and capped exponential retry backoff after a failed drain pass. |
|
||||
|
||||
### `LocalDb` (pair-local config cache — driver role only)
|
||||
|
||||
Driver-role nodes keep a consolidated **`ZB.MOM.WW.LocalDb`** SQLite database that caches the deployed
|
||||
|
||||
@@ -142,6 +142,47 @@ source it from there.
|
||||
|
||||
---
|
||||
|
||||
## ContinuousHistorization configuration
|
||||
|
||||
`ContinuousHistorizationOptions` binds the `ContinuousHistorization` section. It carries **only** the
|
||||
recorder + outbox knobs — the gateway connection (endpoint / key / TLS) is sourced from
|
||||
`ServerHistorian` (above), never restated here.
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `Enabled` | bool | `false` | `true` **and** a configured `ServerHistorian` wires the FasterLog outbox + gateway value-writer and spawns the `ContinuousHistorizationRecorder`. `false` → no recorder; driver tag values are not historized. |
|
||||
| `OutboxPath` | string | `""` | **Directory** holding the FasterLog segment + commit files. Required when `Enabled`. In production set an **absolute** path on durable storage — a relative path resolves against the host's working directory. |
|
||||
| `CommitMode` | string | `PerEntry` | `PerEntry` fsyncs before the append returns (no loss window); `Periodic` batches commits onto a background timer every `CommitIntervalMs`. Parsed case-insensitively; an unrecognized value falls back to `PerEntry`. **See the batch-commit note below — `PerEntry` is one fsync per ingest *burst*, not per value.** |
|
||||
| `CommitIntervalMs` | int | `100` | Periodic-mode commit cadence (ms); must be `> 0` under `Periodic`, ignored under `PerEntry`. |
|
||||
| `DrainBatchSize` | int | `1024` | Outbox entries peeked + written per drain pass. **Raised from 64**: the peek is an in-memory scan of the outbox's live index (widening it costs allocation, not I/O), and a pass is now bounded-parallel over `DrainConcurrency` rather than sequential, so a wide batch no longer costs `tags × latency`. At 64 a drain drew down at most 64 entries every `DrainIntervalSeconds` — below the arrival rate of even a modest historized tag set — so any gateway hiccup left the outbox permanently behind, growing toward `Capacity` and shedding oldest samples instead of catching up. |
|
||||
| `DrainIntervalSeconds` | double | `2` | Steady drain cadence (and the post-success reschedule). |
|
||||
| `DrainConcurrency` | int | `8` | Historian tags a single drain pass writes **concurrently**. One peeked batch is grouped per historian tag and each group is an independent gateway round-trip, so a sequential pass cost `tags × latency`; this window makes it `⌈tags / concurrency⌉ × latency` while bounding how many round-trips can be outstanding against the gateway at once. `1` restores the fully sequential drain; a value `< 1` is clamped to 1 (with a `Validate()` warning). **The ack is unchanged** — still all-or-nothing after every group has reported, and the #538 per-tag quarantine still keys on "another group wrote this pass". |
|
||||
| `Capacity` | int | `500000` | Max un-acked outbox entries before the drop-oldest policy sheds the front of the queue; `0` = unbounded. Roughly 50 MB of FasterLog. The #538 **disk backstop** for the one case the per-tag quarantine deliberately cannot resolve — a historian unreachable or refusing *every* write, where nothing proves any particular tag is at fault so the recorder correctly retains. |
|
||||
| `MaxWriteAttempts` | int | `5` | #538 poison-tag budget: consecutive failed drain passes one historian tag may accumulate before its queued entries are abandoned and the FIFO head is allowed to advance. **Counted only across passes where *other* tags wrote successfully**, so an ordinary gateway outage never advances it. Clamped to at least 1; each abandonment logs a Warning naming the tag. |
|
||||
| `MinBackoffSeconds` | double | `1` | Initial retry backoff after a failed drain pass. |
|
||||
| `MaxBackoffSeconds` | double | `30` | Cap on the exponential retry backoff. |
|
||||
|
||||
> **`PerEntry` now means one fsync per ingest *burst*, not per value.** The recorder appends a whole
|
||||
> value burst through `IHistorizationOutbox.AppendBatchAsync`, which enqueues every entry and then
|
||||
> issues a **single** group commit — so a burst of N values costs one fsync rather than N. The
|
||||
> durability contract is unchanged: the call does not return until that commit completes, so nothing
|
||||
> the caller has been told is buffered can be lost to an unclean restart. (The documented residual is
|
||||
> unchanged too: a throw *before* the commit leaves the enqueued prefix tracked-but-uncommitted —
|
||||
> served correctly for the life of the process, absent after an unclean restart — exactly the
|
||||
> enqueue-then-commit window the single-entry path always had, widened from one entry to the batch.)
|
||||
|
||||
> **Cross-repo follow-up — a multi-tag batch `WriteLiveValues` on the gateway contract.** The drain's
|
||||
> unit of work is one gateway round-trip **per historian tag**, because
|
||||
> `ZB.MOM.WW.HistorianGateway.Client` **0.3.0** exposes no RPC that writes values for more than one tag
|
||||
> in a call. Bounded parallelism (`DrainConcurrency`) is therefore the ceiling: it overlaps the
|
||||
> round-trips but cannot remove them, so a wide tag set still pays `⌈tags / concurrency⌉` network
|
||||
> hops per drain pass. Collapsing that to a single hop needs a **batch/multi-tag `WriteLiveValues`** on
|
||||
> the `historian_gateway.v1` contract in the `~/Desktop/HistorianGateway` sister repo. Until that
|
||||
> lands, raising `DrainBatchSize` widens each hop's payload but not the hop count — which is why the
|
||||
> concurrency window, not the batch width, is what governs drain throughput today.
|
||||
|
||||
---
|
||||
|
||||
## A poison tag no longer pins the outbox (#538)
|
||||
|
||||
`DrainOnceAsync` peeks a batch, writes **every** per-tag group in it, and then acks **all-or-nothing** —
|
||||
|
||||
Reference in New Issue
Block a user