112 lines
6.6 KiB
C#
112 lines
6.6 KiB
C#
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);
|
||
}
|