AE tests: create ConversionTests keys once per class to slow error 3807 identifier exhaustion - #4478
AE tests: create ConversionTests keys once per class to slow error 3807 identifier exhaustion#4478paulmedynski wants to merge 8 commits into
Conversation
Adds tools/testsql/dropOrphanedAlwaysEncryptedKeys.sql to drop leaked Always Encrypted test keys (CMKs/CEKs) and their dependent tables. Leaked keys accumulate on long-lived shared test servers (e.g. the Kerberos CI instance) until the CEK identifier range is exhausted, causing 'Create failed because all available identifiers have been exhausted' during AE test setup. Targets only AE_/AE- prefixed test objects and defaults to preview mode.
There was a problem hiding this comment.
Pull request overview
Adds a standalone T-SQL maintenance script to clean up orphaned Always Encrypted test artifacts (AE-prefixed CMKs/CEKs and dependent tables) in long-lived shared CI databases, mitigating “identifier exhaustion” failures during AE test setup.
Changes:
- Introduces
dropOrphanedAlwaysEncryptedKeys.sqlwith a default “WhatIf” preview mode that printsDROPstatements. - Implements dependency-ordered cleanup (tables → CEKs → CMKs) with per-object
TRY/CATCHto continue on failures. - Provides a summary and reports any remaining AE-prefixed keys after execution.
Phase 1 of fixing 'Create failed because all available identifiers have been exhausted' in the AE manual tests: - Add AlwaysEncryptedCleanup helper: DropSafely (best-effort per-object drop that never aborts) and DropOrphanedAeArtifacts (idempotent sweep of AE_/AE- prefixed tables, CEKs, CMKs). - Make CMK/CEK/Table Drop idempotent (guarded existence checks) so a missing/already-dropped object cannot throw and abort teardown. - Roll back partially-created objects when SQLSetupStrategy.SetupDatabase or the ConversionTests constructor fails, stopping the CMK leak that occurred every time a CEK create failed. - Route all AE fixture teardown through DropSafely with per-connection isolation so remaining keys are always dropped regardless of outcome.
Phase 2: add AlwaysEncryptedCleanup.SweepOrphansOnce, a one-time per-process sweep of orphaned AE_/AE- prefixed tables, CEKs, and CMKs left by earlier runs that were cancelled or hard-killed before teardown. Invoked before key creation in SQLSetupStrategy.SetupDatabase and the ConversionTests constructor. The ManualTests assembly runs serially (XUnitAssemblyAttributes), so the sweep cannot race with keys created by the current run. Phase 3: add server-independent AlwaysEncryptedCleanupTests verifying DropSafely attempts every object even when one drop throws (the leak this change prevents) and tolerates an empty sequence.
Drop AlwaysEncryptedCleanupTests; we don't need tests that test the test cleanup helpers. The cleanup is validated by real AE manual-test runs on the pipeline.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/AlwaysEncryptedCleanup.cs:97
- In the orphan-sweep T-SQL, the cursor for tables selects a schema-qualified, QUOTENAME'd identifier (e.g. [schema].[table]). Storing that in
@name sysname(nvarchar(128)) can truncate longer schema-qualified names, leading to invalid DROP statements and leaving orphans behind. Use a wider NVARCHAR for@name(or separate@tableName/@keyNamevariables).
SET NOCOUNT ON;
DECLARE @name sysname, @sql nvarchar(max);
DropOrphanedAeArtifacts now counts and returns how many tables, CEKs, and CMKs it removed, and SweepOrphansOnce writes a one-line summary per server so a pipeline run shows the identifier recovery happening (or confirms there were no orphans).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (2)
tools/testsql/dropOrphanedAlwaysEncryptedKeys.sql:55
@labelis declared as nvarchar(400) but is assigned values from @dropTables.tableName (nvarchar(517)). If schema/table names are long, this can truncate the identifier and generate invalid DROP statements. Use nvarchar(517) for@labelto match the max length of schema-qualified QUOTENAME output.
DECLARE @stmt nvarchar(max);
DECLARE @label nvarchar(400);
DECLARE @errMsg nvarchar(4000);
src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/AlwaysEncryptedCleanup.cs:128
- The T-SQL sweep uses
DECLARE @name sysname, but the table cursor stores a schema-qualified, QUOTENAME'd name which can be up to 517 characters. Using sysname (128) risks truncation and incomplete cleanup for long names.
DECLARE @name sysname, @sql nvarchar(max);
Root cause of 'Create failed because all available identifiers have been exhausted' on the long-lived Kerberos test DB: SQL Server assigns each CMK/CEK an identifier from a monotonic per-database counter that DROP never reclaims. ConversionTests created a fresh CMK+CEK in its constructor, and xUnit constructs a test class instance per test case, so each run issued dozens of CREATE COLUMN MASTER/ENCRYPTION KEY statements, steadily advancing the counter until it hit error 3807. Move the CMK/CEK creation into a new ConversionTestFixture class fixture so the keys are created once per class instead of once per test case (~two orders of magnitude fewer key creations per run). Per-test tables are still created/dropped per case. This also reverts the earlier orphan-sweep/leak-cleanup approach (AlwaysEncryptedCleanup, idempotent drops, resilient teardown, and the dropOrphanedAlwaysEncryptedKeys.sql script), which targeted key leakage rather than the identifier-counter growth that actually causes 3807.
- ConversionTests: remove using directives orphaned by the fixture refactor (System.Security.Cryptography.X509Certificates and Microsoft.Data.SqlClient.Tests.Common.Fixtures) to avoid unused-using diagnostics (CS8019). (PR #4478 discussion r3658797866) - ConversionTestFixture.Dispose: normalize ConnectTimeout to a 30s minimum like the constructor, so timeout-prone AE teardown is less likely to be skipped and leave the shared CMK/CEK behind. (PR #4478 discussion r3658797921)
Wrap ConversionTestFixture.Dispose DB cleanup in try/finally and treat CMK/CEK drops as best-effort per connection, so base.Dispose (which removes the test certificate from the store) always runs even if a drop or connection open throws. (PR #4478 discussion r3658967825)
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4478 +/- ##
==========================================
- Coverage 64.64% 62.90% -1.75%
==========================================
Files 288 283 -5
Lines 44009 66928 +22919
==========================================
+ Hits 28451 42101 +13650
- Misses 15558 24827 +9269
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
cheenamalhotra
left a comment
There was a problem hiding this comment.
Must consider backporting to release branches as well.
Description
Fixes the recurring
Create failed because all available identifiers have been exhausted.(SQL Server error 3807) failures in the Always Encrypted (AE) manual tests, seen on thesqlclient-ci-kerberospipeline.Root cause
SQL Server assigns each Column Master Key (CMK) and Column Encryption Key (CEK) an identifier from a monotonic, per-database counter that
DROPnever reclaims. Diagnosis on the shared Kerberos server confirmed this: with 0 live keys, the CMK counter was already at 143,789, andCREATE COLUMN ENCRYPTION KEYfailed with 3807 — while the same statement in a fresh database succeeded withid=1. So the exhausted resource is the counter, not accumulated/leaked keys, and no amount of key cleanup can fix it.The dominant consumer of that counter is
ConversionTests: it created a fresh CMK+CEK in its constructor, and xUnit constructs a test-class instance per test case. With ~79 data rows across 3 theories, each run issued dozens ofCREATE COLUMN MASTER/ENCRYPTION KEYstatements from this one class, steadily advancing the counter until it hit the ceiling on the long-lived server. (Every other AE class already creates its keys once, via anIClassFixture.)Change
ConversionTestFixture(anIClassFixture) that creates the CMK+CEK once per test class and drops them on dispose.ConversionTeststo consume that fixture instead of creating keys in its per-test constructor. Per-test tables are still created/dropped per case (tables don't consume the constrained identifier).This reduces this class's key-creation churn by roughly two orders of magnitude, dramatically slowing counter growth on any persistent AE database (both the Kerberos Northwind server and the enclave CI servers, which are on the same trajectory).
Notes / scope
sqldrv-sql22to unblock the pipeline. AE databases require external environment setup that can't currently be provisioned per-run, so a fresh-DB-per-run approach isn't viable.AlwaysEncryptedCleanup, idempotent drops, resilient teardown, and adropOrphanedAlwaysEncryptedKeys.sqlscript). Those targeted key leakage, which turned out not to be the cause of 3807, so they have been reverted. The net change of this PR is just the two files above.Issues
Diagnosed from Kerberos pipeline runs
20260724.1and20260727.3.Testing