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:
co-authored by
Claude Opus 5.5
parent
f8d0ce94fe
commit
5b8656d969
@@ -157,7 +157,10 @@ public sealed class BackupService
|
|||||||
var connectionString = new SqliteConnectionStringBuilder
|
var connectionString = new SqliteConnectionStringBuilder
|
||||||
{
|
{
|
||||||
DataSource = sourcePath,
|
DataSource = sourcePath,
|
||||||
Mode = SqliteOpenMode.ReadOnly
|
Mode = SqliteOpenMode.ReadOnly,
|
||||||
|
// Eine einmalige Kopie: Aus dem Pool heraus hielte die Verbindung die Datei
|
||||||
|
// danach offen, und unter Windows ließe sie sich nicht mehr ersetzen oder löschen.
|
||||||
|
Pooling = false
|
||||||
}.ToString();
|
}.ToString();
|
||||||
|
|
||||||
using var conn = new SqliteConnection(connectionString);
|
using var conn = new SqliteConnection(connectionString);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
namespace ClawdDotNet.Core.Storage;
|
namespace ClawdDotNet.Core.Storage;
|
||||||
@@ -19,6 +20,9 @@ public sealed class SqliteStorage
|
|||||||
private readonly string _connectionString;
|
private readonly string _connectionString;
|
||||||
private readonly SemaphoreSlim _writeGate = new(1, 1);
|
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 string DatabasePath { get; }
|
||||||
|
|
||||||
public SqliteStorage(string databasePath)
|
public SqliteStorage(string databasePath)
|
||||||
@@ -37,6 +41,7 @@ public sealed class SqliteStorage
|
|||||||
Pooling = true,
|
Pooling = true,
|
||||||
DefaultTimeout = 30
|
DefaultTimeout = 30
|
||||||
}.ToString();
|
}.ToString();
|
||||||
|
OpenedDatabases[Path.GetFullPath(databasePath)] = _connectionString;
|
||||||
|
|
||||||
Initialize();
|
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()
|
public SqliteConnection OpenConnection()
|
||||||
{
|
{
|
||||||
var connection = new SqliteConnection(_connectionString);
|
var connection = new SqliteConnection(_connectionString);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public sealed class TaskAdministrationTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_dir);
|
||||||
try { Directory.Delete(_dir, recursive: true); }
|
try { Directory.Delete(_dir, recursive: true); }
|
||||||
catch { /* Aufraeumen ist Nebensache */ }
|
catch { /* Aufraeumen ist Nebensache */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public sealed class AuditRepositoryTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||||
try { Directory.Delete(_directory, recursive: true); }
|
try { Directory.Delete(_directory, recursive: true); }
|
||||||
catch { /* Aufräumen ist Nebensache */ }
|
catch { /* Aufräumen ist Nebensache */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public sealed class BackupServiceTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_root);
|
||||||
try { Directory.Delete(_root, recursive: true); }
|
try { Directory.Delete(_root, recursive: true); }
|
||||||
catch { /* Aufräumen ist Nebensache */ }
|
catch { /* Aufräumen ist Nebensache */ }
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ public sealed class BackupServiceTests : IDisposable
|
|||||||
}, default);
|
}, default);
|
||||||
|
|
||||||
// Verbindungen schließen, damit VACUUM INTO auf eine ruhige Datei trifft.
|
// Verbindungen schließen, damit VACUUM INTO auf eine ruhige Datei trifft.
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_instanceDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ZipPath => Path.Combine(_root, "sicherung.zip");
|
private string ZipPath => Path.Combine(_root, "sicherung.zip");
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public sealed class LicenseBackupTests : IDisposable
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
SecretKeyStore.UseDirectory(null);
|
SecretKeyStore.UseDirectory(null);
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_root);
|
||||||
try { Directory.Delete(_root, recursive: true); }
|
try { Directory.Delete(_root, recursive: true); }
|
||||||
catch { /* Aufräumen ist Nebensache */ }
|
catch { /* Aufräumen ist Nebensache */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public sealed class BudgetGuardTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||||
try { Directory.Delete(_directory, recursive: true); }
|
try { Directory.Delete(_directory, recursive: true); }
|
||||||
catch { /* Aufräumen ist Nebensache */ }
|
catch { /* Aufräumen ist Nebensache */ }
|
||||||
}
|
}
|
||||||
@@ -220,7 +220,7 @@ public sealed class UsageRepositoryTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||||
try { Directory.Delete(_directory, recursive: true); }
|
try { Directory.Delete(_directory, recursive: true); }
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public sealed class MemoryRepositoryTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||||
try { Directory.Delete(_directory, recursive: true); }
|
try { Directory.Delete(_directory, recursive: true); }
|
||||||
catch { /* Aufräumen ist Nebensache */ }
|
catch { /* Aufräumen ist Nebensache */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public sealed class StagingRepositoryTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||||
try { Directory.Delete(_directory, recursive: true); }
|
try { Directory.Delete(_directory, recursive: true); }
|
||||||
catch { /* Aufräumen ist Nebensache */ }
|
catch { /* Aufräumen ist Nebensache */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public sealed class StagingServiceTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||||
try { Directory.Delete(_directory, recursive: true); }
|
try { Directory.Delete(_directory, recursive: true); }
|
||||||
catch { /* Aufräumen ist Nebensache */ }
|
catch { /* Aufräumen ist Nebensache */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public sealed class WerkzeugrechteTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||||
try { Directory.Delete(_directory, recursive: true); }
|
try { Directory.Delete(_directory, recursive: true); }
|
||||||
catch { /* Aufräumen ist Nebensache */ }
|
catch { /* Aufräumen ist Nebensache */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using ClawdDotNet.Core.Storage;
|
||||||
|
using Shouldly;
|
||||||
|
|
||||||
|
namespace ClawdDotNet.Core.Tests.Storage;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releasing the pool of one database must leave every other database alone. The tests used
|
||||||
|
/// the process-wide ClearAllPools to clean up, and a test running in parallel lost its
|
||||||
|
/// connection in the middle of opening it — one red run in every few, always another test.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PoolReleaseTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _root = Path.Combine(Path.GetTempPath(), "clawd-tests", "pool-" + Guid.NewGuid().ToString("N")[..8]);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
SqliteStorage.ReleasePooledConnections(_root);
|
||||||
|
try { Directory.Delete(_root, recursive: true); }
|
||||||
|
catch { /* cleaning up is secondary */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task TouchAsync(SqliteStorage storage)
|
||||||
|
=> await storage.WriteAsync(async connection =>
|
||||||
|
{
|
||||||
|
await using var cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText = "INSERT OR REPLACE INTO ToolState (Key, Value) VALUES ('k', 'v')";
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}, default);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Releasing_one_directory_frees_its_files_and_nothing_else()
|
||||||
|
{
|
||||||
|
var first = new SqliteStorage(Path.Combine(_root, "a", "state.db"));
|
||||||
|
var second = new SqliteStorage(Path.Combine(_root, "b", "state.db"));
|
||||||
|
await TouchAsync(first);
|
||||||
|
await TouchAsync(second);
|
||||||
|
|
||||||
|
SqliteStorage.ReleasePooledConnections(Path.Combine(_root, "a"));
|
||||||
|
|
||||||
|
// The released database can go — on Windows an open handle would prevent that.
|
||||||
|
Should.NotThrow(() => Directory.Delete(Path.Combine(_root, "a"), recursive: true));
|
||||||
|
|
||||||
|
// The other one keeps working, and so does its pool.
|
||||||
|
await TouchAsync(second);
|
||||||
|
await using var connection = await second.OpenConnectionAsync(default);
|
||||||
|
await using var cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText = "SELECT Value FROM ToolState WHERE Key = 'k'";
|
||||||
|
(await cmd.ExecuteScalarAsync()).ShouldBe("v");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task A_database_file_can_be_named_directly()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(_root, "c", "state.db");
|
||||||
|
await TouchAsync(new SqliteStorage(path));
|
||||||
|
|
||||||
|
SqliteStorage.ReleasePooledConnections(path);
|
||||||
|
|
||||||
|
Should.NotThrow(() => File.Delete(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ public sealed class BudgetMissTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||||
try { Directory.Delete(_directory, recursive: true); }
|
try { Directory.Delete(_directory, recursive: true); }
|
||||||
catch { /* cleanup is secondary */ }
|
catch { /* cleanup is secondary */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public sealed class CoordinationMigrationTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||||
try { Directory.Delete(_directory, recursive: true); }
|
try { Directory.Delete(_directory, recursive: true); }
|
||||||
catch { /* Aufräumen ist Nebensache */ }
|
catch { /* Aufräumen ist Nebensache */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public sealed class ScannerCapacityTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||||
try { Directory.Delete(_directory, recursive: true); }
|
try { Directory.Delete(_directory, recursive: true); }
|
||||||
catch { /* cleanup is secondary */ }
|
catch { /* cleanup is secondary */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public sealed class TaskRepositoryTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||||
try { Directory.Delete(_directory, recursive: true); }
|
try { Directory.Delete(_directory, recursive: true); }
|
||||||
catch { /* Aufräumen ist Nebensache */ }
|
catch { /* Aufräumen ist Nebensache */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public sealed class TaskScannerTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||||
try { Directory.Delete(_directory, recursive: true); }
|
try { Directory.Delete(_directory, recursive: true); }
|
||||||
catch { /* Aufräumen ist Nebensache */ }
|
catch { /* Aufräumen ist Nebensache */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public sealed class TaskboardServiceTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||||
try { Directory.Delete(_directory, recursive: true); }
|
try { Directory.Delete(_directory, recursive: true); }
|
||||||
catch { /* Aufräumen ist Nebensache */ }
|
catch { /* Aufräumen ist Nebensache */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public sealed class PromptHistoryTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||||
try { Directory.Delete(_directory, recursive: true); }
|
try { Directory.Delete(_directory, recursive: true); }
|
||||||
catch { /* cleanup is best effort */ }
|
catch { /* cleanup is best effort */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public sealed class ManagerBackupsTests : IAsyncDisposable
|
|||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_root);
|
||||||
try { Directory.Delete(_root, recursive: true); }
|
try { Directory.Delete(_root, recursive: true); }
|
||||||
catch { /* Ein liegengebliebener Testordner faerbt keinen Lauf rot. */ }
|
catch { /* Ein liegengebliebener Testordner faerbt keinen Lauf rot. */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public sealed class MemoryToolTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_dir);
|
||||||
try { Directory.Delete(_dir, recursive: true); }
|
try { Directory.Delete(_dir, recursive: true); }
|
||||||
catch { /* Aufraeumen ist Nebensache */ }
|
catch { /* Aufraeumen ist Nebensache */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public sealed class AssignmentReachTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory);
|
||||||
try { Directory.Delete(_directory, recursive: true); }
|
try { Directory.Delete(_directory, recursive: true); }
|
||||||
catch { /* cleanup is secondary */ }
|
catch { /* cleanup is secondary */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public sealed class DublettenTests : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_verzeichnis);
|
||||||
try { Directory.Delete(_verzeichnis, recursive: true); }
|
try { Directory.Delete(_verzeichnis, recursive: true); }
|
||||||
catch { /* Aufräumen ist Nebensache */ }
|
catch { /* Aufräumen ist Nebensache */ }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user