chore(deps): LocalDb 0.2.0 — dereg cleanup, late-opt-in baselining, byte-budget replication
This commit is contained in:
@@ -233,6 +233,12 @@ public class SiteHealthCheckTests : IDisposable
|
||||
public ReplicatedTable RegisterReplicated(string tableName) =>
|
||||
throw new InvalidOperationException("store unreachable");
|
||||
|
||||
public ReplicatedTable RegisterReplicated(string tableName, bool baselineExistingRows) =>
|
||||
throw new InvalidOperationException("store unreachable");
|
||||
|
||||
public bool DeregisterReplicated(string tableName) =>
|
||||
throw new InvalidOperationException("store unreachable");
|
||||
|
||||
public IReadOnlyDictionary<string, ReplicatedTable> ReplicatedTables =>
|
||||
throw new InvalidOperationException("store unreachable");
|
||||
}
|
||||
|
||||
@@ -104,6 +104,81 @@ public class SiteLocalDbCdcRegistrationTests : IDisposable
|
||||
Assert.DoesNotContain(triggers, t => t.StartsWith("__localdb_smtp_configurations_", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnreplicatedNode_DropsTriggersLeftByAPreviouslyReplicatedBuild()
|
||||
{
|
||||
// WP3.3, the residual WP1.3 could only document: skipping registration never removed
|
||||
// what an EARLIER build installed, so a long-lived unreplicated node upgraded in place
|
||||
// kept capturing forever. LocalDb 0.2.0's DeregisterReplicated closes it, and this is
|
||||
// the exact upgrade shape — the same file, booted first WITH replication configured and
|
||||
// then without. The first boot is what makes the assertion meaningful: without it an
|
||||
// empty trigger set would prove nothing.
|
||||
var replicated = BuildDatabase(Config(apiKey: "cdc-test-key"));
|
||||
AssertCaptureTriggersCoverTheReplicatedTables(replicated);
|
||||
DisposeProviders();
|
||||
|
||||
var unreplicated = BuildDatabase(Config());
|
||||
|
||||
Assert.Empty(CaptureTriggers(unreplicated));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnreplicatedNode_PrunesTheOplogLeftByAPreviouslyReplicatedBuild()
|
||||
{
|
||||
// Deregistration is not only about the trigger cost: the rows already captured are an
|
||||
// oplog nothing will ever drain, so they are pruned with the triggers. Written through
|
||||
// the schema's own table so the capture path is the real one.
|
||||
var replicated = BuildDatabase(Config(apiKey: "cdc-test-key"));
|
||||
InsertSiteEvent(replicated, "evt-1");
|
||||
Assert.NotEqual(0, OplogRowCount(replicated));
|
||||
DisposeProviders();
|
||||
|
||||
var unreplicated = BuildDatabase(Config());
|
||||
|
||||
Assert.Equal(0, OplogRowCount(unreplicated));
|
||||
|
||||
// The table's own data is never touched — deregistration removes bookkeeping, not rows.
|
||||
Assert.Equal(1, SiteEventCount(unreplicated));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnreplicatedNode_OverAFileThatWasNeverRegistered_IsANoOp()
|
||||
{
|
||||
// DeregisterReplicated is idempotent and usable on a table this process never
|
||||
// registered, so the clean-up branch must be harmless on the ordinary case: site-b and
|
||||
// site-c boot after boot with nothing to clean.
|
||||
_ = BuildDatabase(Config());
|
||||
DisposeProviders();
|
||||
|
||||
var second = BuildDatabase(Config());
|
||||
|
||||
Assert.Empty(CaptureTriggers(second));
|
||||
foreach (var table in AllSiteTables)
|
||||
Assert.Contains(table, TableNames(second));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplicatedNode_BaselinesRowsThatPredateRegistration()
|
||||
{
|
||||
// The other half of the late-opt-in story: a site that has been running unreplicated
|
||||
// and then turns replication ON. The snapshot streamer pages from __localdb_row_version,
|
||||
// which only the capture triggers populate, so without baselining these rows would be
|
||||
// invisible to the peer forever. RegisterReplicated(..., baselineExistingRows: true)
|
||||
// seeds them at the LWW floor (HLC 0).
|
||||
var unreplicated = BuildDatabase(Config());
|
||||
Assert.Empty(CaptureTriggers(unreplicated));
|
||||
InsertSiteEvent(unreplicated, "pre-existing");
|
||||
Assert.Equal(0, RowVersionCount(unreplicated, "site_events"));
|
||||
DisposeProviders();
|
||||
|
||||
var replicated = BuildDatabase(Config(apiKey: "cdc-test-key"));
|
||||
|
||||
Assert.Equal(1, RowVersionCount(replicated, "site_events"));
|
||||
|
||||
// At the floor, so the seeded version loses to any genuine remote write of the same key.
|
||||
Assert.Equal(0L, RowVersionHlc(replicated, "site_events"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnreplicatedNode_StillRunsTheLegacyMigrator()
|
||||
{
|
||||
@@ -149,6 +224,49 @@ public class SiteLocalDbCdcRegistrationTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes every database built so far and releases the pooled connections, so the NEXT
|
||||
/// <see cref="BuildDatabase"/> on the same file opens it fresh — which is what a node
|
||||
/// restarting with different replication configuration actually does.
|
||||
/// </summary>
|
||||
private void DisposeProviders()
|
||||
{
|
||||
foreach (var provider in _providers) provider.Dispose();
|
||||
_providers.Clear();
|
||||
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
|
||||
}
|
||||
|
||||
private static void InsertSiteEvent(ILocalDb db, string id)
|
||||
{
|
||||
using var connection = db.CreateConnection();
|
||||
using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText =
|
||||
"""
|
||||
INSERT INTO site_events (id, timestamp, event_type, severity, source, message)
|
||||
VALUES ($id, '2026-08-14T00:00:00.0000000Z', 'Test', 'Info', 'cdc-test', 'row');
|
||||
""";
|
||||
cmd.Parameters.AddWithValue("$id", id);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static long OplogRowCount(ILocalDb db) => Scalar(db, "SELECT COUNT(*) FROM __localdb_oplog");
|
||||
|
||||
private static long SiteEventCount(ILocalDb db) => Scalar(db, "SELECT COUNT(*) FROM site_events");
|
||||
|
||||
private static long RowVersionCount(ILocalDb db, string table) => Scalar(
|
||||
db, $"SELECT COUNT(*) FROM __localdb_row_version WHERE table_name = '{table}'");
|
||||
|
||||
private static long RowVersionHlc(ILocalDb db, string table) => Scalar(
|
||||
db, $"SELECT MAX(hlc) FROM __localdb_row_version WHERE table_name = '{table}'");
|
||||
|
||||
private static long Scalar(ILocalDb db, string sql)
|
||||
{
|
||||
using var connection = db.CreateConnection();
|
||||
using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
return Convert.ToInt64(cmd.ExecuteScalar());
|
||||
}
|
||||
|
||||
private ILocalDb BuildDatabase(IConfiguration config)
|
||||
{
|
||||
var provider = new ServiceCollection()
|
||||
|
||||
Reference in New Issue
Block a user