Files
scadaproj/ZB.MOM.WW.Secrets/tests/ZB.MOM.WW.Secrets.Replicator.SqlServer.Tests/SqlServerSecretsStoreMigratorTests.cs
T
Joseph Doherty 31ca9407a6 fix(secrets-sqlserver): retry the virgin-DB concurrent migrator race; release family 0.5.1
The ScadaBridge central shared-store live gate proved SqlServerSecretsStoreMigrator's
"safe to run concurrently" claim false on a virgin database: Serializable isolation does
not serialize the IF NOT EXISTS metadata probes against another session's in-flight
CREATE, so of two central nodes booting together the loser died with SqlException 2714
("There is already an object named 'zbsecrets'") — the retry filter covered only
deadlock 1205. Losing such a race means the object now exists (the desired end state),
so 2714 (schema/table), 1913 (index) and 2627 (schema_version MERGE-insert PK) are now
retryable alongside 1205; the retry re-runs the idempotent IF NOT EXISTS path.
Classification scans the whole SqlException.Errors collection (2714 arrives stacked with
2759), the retry cap keeps genuine conflicts failing closed, and 2705 is deliberately
excluded (unreachable — columns live in a single CREATE TABLE statement). Predicate
extracted as an internal static pinned by offline tests; live suite gains an env-gated
4-concurrent-migrators virgin-database test. Six packages pushed to the Gitea feed at
0.5.1.

Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1
2026-08-07 11:35:13 -04:00

41 lines
2.3 KiB
C#

namespace ZB.MOM.WW.Secrets.Replicator.SqlServer.Tests;
/// <summary>
/// Offline pins for the migrator's retry classification. A <see cref="Microsoft.Data.SqlClient.SqlException"/>
/// cannot be constructed normally, so the error-number predicate is extracted as an internal static
/// and pinned here by number; the concurrent-provision behaviour itself is exercised by the
/// env-gated live suite.
/// </summary>
public sealed class SqlServerSecretsStoreMigratorTests
{
/// <summary>
/// The provisioning-race outcomes the ScadaBridge central shared-store live gate (2026-08-07)
/// proved reachable — or that the multi-statement DDL makes reachable — when N nodes provision
/// one virgin database at once. Losing such a race means the object now exists, which is the
/// desired end state, so a retry of the idempotent IF NOT EXISTS path must be taken.
/// </summary>
[Theory]
[InlineData(1205)] // deadlock victim (pre-0.5.1 was the ONLY retried number)
[InlineData(2714)] // "There is already an object named ..." — lost CREATE SCHEMA/TABLE (the live-gate defect)
[InlineData(1913)] // "index ... already exists" — lost CREATE INDEX
[InlineData(2627)] // PK violation — lost schema_version MERGE-insert
public void Provisioning_race_error_numbers_are_retryable(int errorNumber)
=> Assert.True(SqlServerSecretsStoreMigrator.IsRetryableMigrationErrorNumber(errorNumber));
/// <summary>
/// Errors that reproduce identically on retry must propagate so the node fails closed instead
/// of spinning through the retry budget against a genuine misconfiguration.
/// </summary>
[Theory]
[InlineData(2705)] // duplicate column — unreachable from the race (single CREATE TABLE statement)
[InlineData(208)] // invalid object name
[InlineData(229)] // permission denied
[InlineData(2759)] // "CREATE SCHEMA failed due to previous errors" — retryable only via its companion 2714
[InlineData(4060)] // cannot open database
[InlineData(18456)] // login failed
[InlineData(0)]
[InlineData(-2)] // client-side timeout
public void Non_race_error_numbers_are_not_retryable(int errorNumber)
=> Assert.False(SqlServerSecretsStoreMigrator.IsRetryableMigrationErrorNumber(errorNumber));
}