11675285af
P4-5: EventLogPurgeService.ExecuteAsync now yields (Task.Yield()) before running the initial RunPurge(), making "the purge never runs synchronously on the caller's thread" a property of the method itself rather than an artifact of .NET 8+'s BackgroundService scheduling (verified empirically not to reproduce the literal "StartAsync blocks" symptom on this repo's net10.0 target — a 1.5s synchronous Thread.Sleep in ExecuteAsync still lets StartAsync return in ~2ms). Proven instead via a reflection-invoked ExecuteAsync test that bypasses BackgroundService's Task.Run wrapper, which is timing-independent. Also: a throwing isActive predicate now fails CLOSED (logged as inactive, at Warning) instead of silently claiming "active" — the predicate only affects the tick's log line since P4-4, but a throw must not masquerade as a successful check. P4-6: PRAGMA auto_vacuum = INCREMENTAL only takes effect on a table-less database, and LocalDbSchema.EnsureCreated (inside SqliteLocalDb's constructor) always creates LocalDb's own bookkeeping tables before SiteEventLogSchema.Apply ever gets a connection — so the pragma there had never once taken effect, in any real call path. Real fix per plan decision 2: SiteLocalDbSetup.EnsureAutoVacuumOnFreshFile pre-creates a genuinely fresh LocalDb file with the pragma set, called from SiteServiceRegistration.Configure BEFORE AddZbLocalDb's lazy singleton ever constructs SqliteLocalDb — no LocalDb API change needed. Only helps NEW site databases; an existing file (every node on the docker rig today) is left untouched rather than VACUUM'd, per the plan's explicit rejection of an unconditional VACUUM against a live ~1 GB file. The now-dead pragma call in SiteEventLogSchema.Apply is removed and its comment corrected. Tests proven to fail against the unfixed code (git-diff-and-restore, not stash — this repo shares one stash ref across all worktrees): ExecuteAsync_YieldsBeforeRunningTheInitial- PurgeSynchronously and RunPurge_ActiveNodeCheckThrows_TickLogsAsInactive_NotActive fail without the EventLogPurgeService fix; SiteLocalDbAutoVacuumTests fails to compile without EnsureAutoVacuumOnFreshFile. SiteEventLogging.Tests 112/112 and Host.Tests 526/526 green with the fix applied.
81 lines
4.4 KiB
C#
81 lines
4.4 KiB
C#
using Microsoft.Data.Sqlite;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.SiteEventLogging;
|
|
|
|
/// <summary>
|
|
/// DDL for the <c>site_events</c> table, extracted from <see cref="SiteEventLogger"/> so
|
|
/// it can be applied by whoever owns the database file.
|
|
/// <para>
|
|
/// Under LocalDb Phase 1 that owner is the Host's <c>AddZbLocalDb</c> onReady callback:
|
|
/// the table must exist before <c>ILocalDb.RegisterReplicated</c> installs its capture
|
|
/// triggers, and rows written before registration are never captured or snapshotted.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Schema change.</b> The primary key is a TEXT GUID minted by the writer, replacing
|
|
/// the historical <c>INTEGER PRIMARY KEY AUTOINCREMENT</c>. This is required, not
|
|
/// cosmetic: replication resolves conflicts by last-writer-wins on the primary key, so
|
|
/// two site nodes each minting autoincrement id 1 for unrelated events would be treated
|
|
/// as one row and silently overwrite each other. GUID keys make the event log a pure
|
|
/// union across the pair. Consumers that relied on the id being monotonic — the keyset
|
|
/// paging cursor and the storage-cap purge — order by <c>timestamp</c> instead.
|
|
/// </para>
|
|
/// <para>
|
|
/// Deliberately depends only on <c>Microsoft.Data.Sqlite</c>, not on the LocalDb
|
|
/// library — the Host needs to apply this DDL to a LocalDb-managed connection, but
|
|
/// nothing about the schema itself is LocalDb-specific.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class SiteEventLogSchema
|
|
{
|
|
/// <summary>
|
|
/// Creates the <c>site_events</c> table and its indexes when absent. Idempotent —
|
|
/// safe to run on every startup.
|
|
/// </summary>
|
|
/// <param name="connection">An open connection to the database that should hold the table.</param>
|
|
public static void Apply(SqliteConnection connection)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(connection);
|
|
|
|
// arch-review 2026-08-19 P4-6: this method used to also set
|
|
// "PRAGMA auto_vacuum = INCREMENTAL" here, on the theory that setting it right
|
|
// before CREATE TABLE would make it take effect on a fresh database. That was
|
|
// wrong in every real call path: SQLite only honors an auto_vacuum change on a
|
|
// database that has NO tables yet, and by the time this method runs — via the
|
|
// Host's AddZbLocalDb onReady callback (SiteLocalDbSetup.OnReady) OR via
|
|
// SiteEventLogger's own InitializeSchema fallback for directly-constructed
|
|
// instances — the consolidated file already holds LocalDb's own bookkeeping
|
|
// tables (SqliteLocalDb's constructor runs LocalDbSchema.EnsureCreated on its
|
|
// master connection before either caller ever gets a connection). So the pragma
|
|
// here had never once taken effect; it was a silent, permanent no-op. The real
|
|
// fix now lives in Host/SiteLocalDbSetup.cs
|
|
// (EnsureAutoVacuumOnFreshFile), which pre-creates a genuinely fresh LocalDb file
|
|
// with the pragma set BEFORE AddZbLocalDb's lazy singleton ever constructs
|
|
// SqliteLocalDb — and, being a Host-layer, one-time, pre-file-existence step, it
|
|
// helps ONLY brand-new site databases; every file created before this fix shipped
|
|
// stays auto_vacuum=NONE forever (see the PurgeByStorageCap remarks in
|
|
// EventLogPurgeService for the operational consequence).
|
|
using var cmd = connection.CreateCommand();
|
|
cmd.CommandText = """
|
|
CREATE TABLE IF NOT EXISTS site_events (
|
|
id TEXT NOT NULL PRIMARY KEY,
|
|
timestamp TEXT NOT NULL,
|
|
event_type TEXT NOT NULL,
|
|
severity TEXT NOT NULL,
|
|
instance_id TEXT,
|
|
source TEXT NOT NULL,
|
|
message TEXT NOT NULL,
|
|
details TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON site_events(timestamp);
|
|
CREATE INDEX IF NOT EXISTS idx_events_type ON site_events(event_type);
|
|
CREATE INDEX IF NOT EXISTS idx_events_instance ON site_events(instance_id);
|
|
CREATE INDEX IF NOT EXISTS idx_events_severity ON site_events(severity);
|
|
""";
|
|
// The query service also supports keyword search via leading-wildcard
|
|
// LIKE on message/source. A leading-wildcard LIKE cannot use a B-tree
|
|
// index, so that path intentionally full-scans; severity/event_type/
|
|
// instance_id/timestamp filters above are all covered.
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
}
|