using Microsoft.Data.Sqlite; using ZB.MOM.WW.LocalDb.Internal; namespace ZB.MOM.WW.LocalDb.Tests; /// /// Sustained concurrent write pressure against a real on-disk LocalDb file — /// the gap named by criterion 5 of /// ScadaBridge/docs/known-issues/2026-07-20-localdb-disk-io-error-under-load.md §9: /// Phase 1 shipped only correctness/convergence tests, so nothing exercised /// many pooled per-operation connections writing a registered (trigger + /// zb_hlc_next UDF) table at once, which is exactly the shape the ScadaBridge /// site nodes run under load. /// public sealed class ConcurrentWriteLoadTests : IDisposable { private readonly string _path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".db"); public void Dispose() { SqliteConnection.ClearAllPools(); foreach (var suffix in new[] { "", "-wal", "-shm" }) { var f = _path + suffix; if (File.Exists(f)) File.Delete(f); } } [Fact] public async Task Sustained_Concurrent_Writes_Through_Pooled_Connections_All_Succeed() { const int Writers = 8; const int PerWriter = 250; using var db = new SqliteLocalDb(new LocalDbOptions { Path = _path }); await db.ExecuteAsync( "CREATE TABLE load_t (id TEXT PRIMARY KEY, writer INTEGER NOT NULL, seq INTEGER NOT NULL, payload TEXT NOT NULL)"); db.RegisterReplicated("load_t"); var payload = new string('x', 256); // Concurrent readers churn pooled connections + the WAL index while the // writers run; WAL readers must never fail alongside writers. using var stopReaders = new CancellationTokenSource(); var reader = Task.Run(async () => { while (!stopReaders.IsCancellationRequested) { await db.QueryAsync("SELECT COUNT(*) FROM load_t", r => r.GetInt64(0)); } }); // One fresh pooled connection per operation (the production pattern), // every insert firing the capture trigger + zb_hlc_next UDF. var writers = Enumerable.Range(0, Writers).Select(w => Task.Run(async () => { for (var i = 0; i < PerWriter; i++) { await db.ExecuteAsync( "INSERT INTO load_t (id, writer, seq, payload) VALUES (@id, @writer, @seq, @payload)", new { id = $"{w}-{i}", writer = w, seq = i, payload }); } })).ToArray(); await Task.WhenAll(writers); // faults on the first failed write stopReaders.Cancel(); await reader; var rows = (await db.QueryAsync("SELECT COUNT(*) FROM load_t", r => r.GetInt64(0)))[0]; Assert.Equal((long)Writers * PerWriter, rows); // Every write must have been captured to the oplog exactly once. var oplog = (await db.QueryAsync("SELECT COUNT(*) FROM __localdb_oplog", r => r.GetInt64(0)))[0]; Assert.Equal((long)Writers * PerWriter, oplog); } }