Tests: Verbindungspools nur der eigenen Datenbank leeren statt aller

Die zufaelligen roten Laeufe der vollen Suite (jedes Mal ein anderer Test,
zuletzt PromptHistoryTests mit einer reinen Funktion) hatten eine Ursache:
Zwanzig Testklassen riefen beim Aufraeumen SqliteConnection.ClearAllPools()
auf. Das ist prozessweit und nahm einem parallel laufenden Test die
Verbindung mitten im Oeffnen weg - ObjectDisposedException in ApplyPragmas.

- SqliteStorage.ReleasePooledConnections(pfad) leert nur die Pools der
  Datenbanken unter einer Datei oder einem Verzeichnis.
- BackupService oeffnet die Quelle fuer VACUUM INTO ohne Pool: eine
  einmalige Kopie soll die Datei danach nicht offen halten.
- PoolReleaseTests: die freigegebene Datenbank laesst sich loeschen, die
  andere arbeitet weiter.

Vier volle Laeufe hintereinander gruen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-24 15:27:33 +02:00
co-authored by Claude Opus 5.5
parent f8d0ce94fe
commit 5b8656d969
23 changed files with 119 additions and 23 deletions
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using Microsoft.Data.Sqlite;
namespace ClawdDotNet.Core.Storage;
@@ -19,6 +20,9 @@ public sealed class SqliteStorage
private readonly string _connectionString;
private readonly SemaphoreSlim _writeGate = new(1, 1);
/// <summary>Every database opened in this process: full path → connection string.</summary>
private static readonly ConcurrentDictionary<string, string> OpenedDatabases = new(StringComparer.OrdinalIgnoreCase);
public string DatabasePath { get; }
public SqliteStorage(string databasePath)
@@ -37,6 +41,7 @@ public sealed class SqliteStorage
Pooling = true,
DefaultTimeout = 30
}.ToString();
OpenedDatabases[Path.GetFullPath(databasePath)] = _connectionString;
Initialize();
@@ -51,6 +56,33 @@ public sealed class SqliteStorage
}
}
/// <summary>
/// Closes the pooled connections of the databases at <paramref name="path"/> — a
/// database file, or a directory and everything below it — so the files can be deleted
/// or replaced.
///
/// <para>Not <see cref="SqliteConnection.ClearAllPools"/>: that is process-wide and also
/// takes the connections of every other database in use. The tests used it to clean up
/// their temporary databases, and a test running in parallel then lost its connection
/// while opening it — an ObjectDisposedException in <c>ApplyPragmas</c>, one in every
/// few full runs, always in a different test.</para>
/// </summary>
public static void ReleasePooledConnections(string path)
{
var full = Path.GetFullPath(path);
var below = full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
foreach (var (database, connectionString) in OpenedDatabases)
{
if (!string.Equals(database, full, StringComparison.OrdinalIgnoreCase)
&& !database.StartsWith(below, StringComparison.OrdinalIgnoreCase))
continue;
using var connection = new SqliteConnection(connectionString);
SqliteConnection.ClearPool(connection);
}
}
public SqliteConnection OpenConnection()
{
var connection = new SqliteConnection(_connectionString);