Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogOptions.cs
T

112 lines
6.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace ZB.MOM.WW.ScadaBridge.SiteEventLogging;
public class SiteEventLogOptions
{
/// <summary>Number of days to retain site event log entries before purge; default 30.</summary>
public int RetentionDays { get; set; } = 30;
/// <summary>
/// Maximum size in megabytes of <c>site_events</c> AND ITS FOUR INDEXES before the oldest
/// entries are trimmed; default 1024 MB.
/// </summary>
/// <remarks>
/// <para>
/// <b>This is not a cap on the database file</b> (register row 81, decision record
/// <c>docs/plans/2026-08-17-site-event-log-cap-semantics-decision.md</c>). <c>site_events</c>
/// shares one SQLite file at <c>LocalDb:Path</c> with nine other tables plus LocalDb's
/// <c>__localdb_oplog</c> / <c>__localdb_row_version</c> bookkeeping. Measuring the whole file
/// made the trim unwinnable on a replication-enabled node — each batch of deletes generated
/// an equal number of oplog rows and tombstones in the same file, so the measured size never
/// fell and the trim stopped 2.5x over cap. Worse, it made the event log's budget a function
/// of the replication peer's health, in the direction where a replication fault destroys
/// operational history.
/// </para>
/// <para>
/// Consequence for sizing a site's data volume: this value alone does not bound it. LocalDb
/// replication bookkeeping is separate overhead on the same disk, bounded by
/// <c>LocalDb:Replication:MaxOplogRows</c> and <c>TombstoneRetention</c>. Budget the sum.
/// </para>
/// <para>
/// The footprint is MODELLED from sampled row width rather than measured exactly: SQLite's
/// <c>dbstat</c> virtual table would give an exact per-table page count but is not compiled
/// into the bundled native library. Measured accuracy is −0.8 % to +12.8 %, deliberately
/// biased high. See <c>EventLogPurgeService.EstimateSiteEventsSize</c>.
/// </para>
/// </remarks>
public int MaxStorageMb { get; set; } = 1024;
/// <summary>
/// MIGRATION-ONLY now that <see cref="SiteEventLogger"/> no longer reads
/// this: <c>site_events</c> lives in the consolidated site database at
/// <c>LocalDb:Path</c>. The only remaining consumer is
/// <c>SiteLocalDbLegacyMigrator</c>, which uses it to locate a pre-consolidation
/// <c>site_events.db</c> and copy it in once. Safe to delete from a config once that
/// node's migration has run.
/// </summary>
public string DatabasePath { get; set; } = "site_events.db";
/// <summary>Maximum number of rows returned per paginated query; default 500.</summary>
public int QueryPageSize { get; set; } = 500;
/// <summary>
/// Hard upper bound on a caller-supplied <c>PageSize</c>. A
/// misbehaving or hostile central client that requests <c>int.MaxValue</c> would
/// otherwise force the query to materialise the entire log into a single list while
/// holding the shared write lock. Silent clamp; default 500 matches
/// <see cref="QueryPageSize"/>.
/// </summary>
public int MaxQueryPageSize { get; set; } = 500;
/// <summary>Interval between purge runs; default 24 hours.</summary>
public TimeSpan PurgeInterval { get; set; } = TimeSpan.FromHours(24);
/// <summary>
/// Ceiling on how many <c>site_events</c> rows ONE purge tick may delete, across the
/// retention purge and the storage-cap trim combined. Default 250 000; <c>0</c> means
/// unlimited (the pre-pacing behaviour, kept as an escape hatch).
/// </summary>
/// <remarks>
/// <para>
/// <b>Why a ceiling exists at all.</b> <c>site_events</c> is CDC-captured on a
/// replication-enabled node and there is deliberately no purge-exemption path, so a purge
/// queues one oplog row per deleted event. Measured worst case (register row 34,
/// <c>docs/plans/2026-08-15-site-events-purge-burst-measurement.md</c>): a 1 GB event log
/// holds ~2.73 million rows at 393 bytes/row, and expiring all of them in one tick queued
/// 2 731 947 oplog rows — <b>2.7x over LocalDb's <c>MaxOplogRows</c> default of
/// 1 000 000</b>. Crossing that ceiling makes LocalDb's maintenance tick prune the oplog
/// and set <c>needs_snapshot</c>, which costs a FULL snapshot resync of all ten replicated
/// tables. The ceiling keeps a single tick's burst safely inside that budget (250 000 is
/// a 4x margin) while <see cref="PurgeCatchUpInterval"/> keeps retention converging.
/// </para>
/// <para>
/// The remainder is not dropped: a tick that hits the ceiling schedules the next tick after
/// <see cref="PurgeCatchUpInterval"/> instead of <see cref="PurgeInterval"/>, so a backlog
/// drains over minutes rather than days while every individual burst stays bounded.
/// </para>
/// </remarks>
public int MaxPurgedRowsPerTick { get; set; } = 250_000;
/// <summary>
/// Delay before the next purge tick when the previous tick stopped at
/// <see cref="MaxPurgedRowsPerTick"/>; default 5 minutes. Ignored when the ceiling was not
/// reached — an ordinary tick waits <see cref="PurgeInterval"/>.
/// </summary>
/// <remarks>
/// Sized so the peer drains one tick's burst before the next one lands: 250 000 tombstones
/// measured at ~10 800 rows/s over a loopback pair (≈23 s), leaving an order of magnitude of
/// headroom for a real inter-node link. A worst-case 2.73 M-row backlog therefore converges
/// in ~11 ticks ≈ 55 minutes instead of 11 days at the daily interval.
/// </remarks>
public TimeSpan PurgeCatchUpInterval { get; set; } = TimeSpan.FromMinutes(5);
/// <summary>
/// Bound on the background write queue. Default 10 000 events.
/// Overflow uses <c>BoundedChannelFullMode.DropOldest</c> — callers never block; the
/// dropped event's <c>Task</c> is faulted and <c>FailedWriteCount</c> is incremented
/// so the drop is observable.
/// </summary>
public int WriteQueueCapacity { get; set; } = 10_000;
/// <summary>
/// Widest time window a <em>keyword</em> search may scan; default 7 days. The keyword filter
/// is a leading-wildcard <c>LIKE</c> over <c>message</c>/<c>source</c> that full-scans the
/// window, so an unbounded search against a log at its storage cap walks millions of rows.
/// A keyword query with no <c>From</c> (or a wider range) is clamped server-side to
/// <c>[To ?? now - window, To ?? now]</c> rather than rejected, so existing callers keep
/// working (review 2026-10-21 02/P1).
/// </summary>
public TimeSpan KeywordSearchMaxWindow { get; set; } = TimeSpan.FromDays(7);
}