From 463aced7a1d958a6d7f165a516b5c63fd971b1b4 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 10 Nov 2026 06:12:34 -0500 Subject: [PATCH] test(inboundapi): root the shared in-memory SQLite keep-alive so the database cannot vanish mid-test Script_UsingDatabase_QueriesViaGateway failed in about 2 of 27 full InboundAPI runs. Looping the suite 30 times captured the failure 4 times, always as "Internal script error". The exception underneath was SqliteException 'no such table: Machine'. Cause: the seeding helper opened its keep-alive SqliteConnection in a local. Once the helper returned, nothing referenced that connection. After a GC (likely during the Roslyn compile, under full-suite load), Microsoft.Data.Sqlite's pool reclaimed the leaked connection and closed the last connection to the shared in-memory database, which destroyed it. Fix: hold the keep-alive in a static field. InboundDatabaseHelperTests had the same latent bug and gets the same fix. The test now forces GC plus SqliteConnection.ClearAllPools() before the query. That makes the old pattern fail deterministically and the fix pass deterministically. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV --- .../InboundDatabaseHelperTests.cs | 20 +++++++++++-- .../InboundScriptExecutorTests.cs | 28 +++++++++++++++++-- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundDatabaseHelperTests.cs b/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundDatabaseHelperTests.cs index b50c2fb3..e9ec4a89 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundDatabaseHelperTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundDatabaseHelperTests.cs @@ -33,11 +33,25 @@ public class InboundDatabaseHelperTests => throw new NotImplementedException(); } - private static SqliteGateway SeededGateway(bool honorTokenOnOpen = true) + /// + /// Keep-alive for the shared in-memory database, rooted in a static field. It used to be a + /// local in the seeding helper: once the helper returned nothing referenced it, so after a GC the + /// Sqlite pool could reclaim the leaked connection, close the last connection and destroy the + /// database before the test's query ran (no such table: Machine). This is the same latent + /// flake the executor tests hit. + /// + private static readonly SqliteConnection KeepAlive = OpenKeepAlive(); + + private static SqliteConnection OpenKeepAlive() { var keep = new SqliteConnection("DataSource=file:movein?mode=memory&cache=shared"); - keep.Open(); // keep-alive: shared in-memory db lives until process exit - using var cmd = keep.CreateCommand(); + keep.Open(); // lives until process exit + return keep; + } + + private static SqliteGateway SeededGateway(bool honorTokenOnOpen = true) + { + using var cmd = KeepAlive.CreateCommand(); cmd.CommandText = "CREATE TABLE IF NOT EXISTS Machine(Code TEXT, SAPID TEXT); DELETE FROM Machine; INSERT INTO Machine VALUES('Z28061A','131453');"; cmd.ExecuteNonQuery(); return new SqliteGateway("DataSource=file:movein?mode=memory&cache=shared", honorTokenOnOpen); diff --git a/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundScriptExecutorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundScriptExecutorTests.cs index 4d47c438..0d8a701c 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundScriptExecutorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/InboundScriptExecutorTests.cs @@ -940,6 +940,13 @@ public class InboundScriptExecutorTests var services = new ServiceCollection(); services.AddSingleton(SeededSqliteGateway()); using var provider = services.BuildServiceProvider(); + // Regression guard for the old flake: force the collection the full suite used to hit during + // the script compile, then the pool reclaim that followed it. An unrooted keep-alive dies here + // deterministically ("Internal script error"); ClearAllPools only closes IDLE connections. + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + SqliteConnection.ClearAllPools(); var executor = new InboundScriptExecutor( NullLogger.Instance, provider); @@ -1052,11 +1059,26 @@ public class InboundScriptExecutorTests => throw new NotImplementedException(); } - private static SqliteGateway SeededSqliteGateway() + /// + /// Keep-alive for the shared in-memory database, rooted in a static field. It used to be a + /// local in the seeding helper: once the helper returned nothing referenced it, so after a GC + /// (likely during the script compile under a full-suite run) Microsoft.Data.Sqlite's pool + /// reclaimed the leaked connection, closed the last connection and destroyed the database. The + /// query then failed with no such table: Machine, which the script reported as "Internal + /// script error" (4 runs in 30, captured 2026-11-10). + /// + private static readonly SqliteConnection KeepAlive = OpenKeepAlive(); + + private static SqliteConnection OpenKeepAlive() { var keep = new SqliteConnection("DataSource=file:movein-exec?mode=memory&cache=shared"); - keep.Open(); // keep-alive: shared in-memory db lives until process exit - using var cmd = keep.CreateCommand(); + keep.Open(); // lives until process exit + return keep; + } + + private static SqliteGateway SeededSqliteGateway() + { + using var cmd = KeepAlive.CreateCommand(); cmd.CommandText = "CREATE TABLE IF NOT EXISTS Machine(Code TEXT, SAPID TEXT); DELETE FROM Machine; INSERT INTO Machine VALUES('Z28061A','131453');"; cmd.ExecuteNonQuery(); return new SqliteGateway("DataSource=file:movein-exec?mode=memory&cache=shared");