Skip to content

Commit 874a92a

Browse files
authored
Enable read committed snapshot on template database (#932)
1 parent 90b58b7 commit 874a92a

8 files changed

Lines changed: 61 additions & 11 deletions

File tree

pages/design.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ The usual approach for consuming the API in a test project is as follows.
4444

4545
This assumes that there is a schema and data (and DbContext in the EntityFramework context) used for all tests. If those caveats are not correct then multiple SqlInstances can be used.
4646

47+
48+
## Template database settings
49+
50+
When the template is built, a few database-level settings are applied. These persist in the template's `.mdf`/`.ldf` files and are inherited by every database attached from the template.
51+
52+
* `auto_update_statistics off` — avoids background statistics updates causing nondeterministic test timing.
53+
* `read_committed_snapshot on``READ COMMITTED` uses row versioning instead of shared locks. Required to prevent S/X-lock deadlocks between parallel `[SharedDbWithTransaction]` tests against the same shared database.
54+
55+
If the template files already exist on disk from a previous build, these settings are not reapplied — the template needs to be regenerated (delete the template files, or change the timestamp passed to `SqlInstance`) for new settings to take effect.
56+
57+
4758
More information:
4859

4960
* [Raw SqlConnection Usage](/pages/raw-usage.md)

pages/ef-usage.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ public async Task SharedDatabase()
292292
AreEqual(0, count);
293293
}
294294
```
295-
<sup><a href='/src/EfLocalDb.Tests/Tests.cs#L674-L684' title='Snippet source file'>snippet source</a> | <a href='#snippet-EfSharedDatabase' title='Start of snippet'>anchor</a></sup>
295+
<sup><a href='/src/EfLocalDb.Tests/Tests.cs#L687-L697' title='Snippet source file'>snippet source</a> | <a href='#snippet-EfSharedDatabase' title='Start of snippet'>anchor</a></sup>
296296
<!-- endSnippet -->
297297

298298
Pass `useTransaction: true` to get an auto-rolling-back transaction, allowing writes without affecting other tests.
@@ -318,7 +318,7 @@ public async Task SharedDatabase_WithTransaction()
318318
AreEqual(0, count);
319319
}
320320
```
321-
<sup><a href='/src/EfLocalDb.Tests/Tests.cs#L698-L716' title='Snippet source file'>snippet source</a> | <a href='#snippet-EfSharedDatabase_WithTransaction' title='Start of snippet'>anchor</a></sup>
321+
<sup><a href='/src/EfLocalDb.Tests/Tests.cs#L711-L729' title='Snippet source file'>snippet source</a> | <a href='#snippet-EfSharedDatabase_WithTransaction' title='Start of snippet'>anchor</a></sup>
322322
<!-- endSnippet -->
323323

324324

pages/mdsource/design.source.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ The usual approach for consuming the API in a test project is as follows.
3737

3838
This assumes that there is a schema and data (and DbContext in the EntityFramework context) used for all tests. If those caveats are not correct then multiple SqlInstances can be used.
3939

40+
41+
## Template database settings
42+
43+
When the template is built, a few database-level settings are applied. These persist in the template's `.mdf`/`.ldf` files and are inherited by every database attached from the template.
44+
45+
* `auto_update_statistics off` — avoids background statistics updates causing nondeterministic test timing.
46+
* `read_committed_snapshot on``READ COMMITTED` uses row versioning instead of shared locks. Required to prevent S/X-lock deadlocks between parallel `[SharedDbWithTransaction]` tests against the same shared database.
47+
48+
If the template files already exist on disk from a previous build, these settings are not reapplied — the template needs to be regenerated (delete the template files, or change the timestamp passed to `SqlInstance`) for new settings to take effect.
49+
50+
4051
More information:
4152

4253
* [Raw SqlConnection Usage](/pages/raw-usage.md)

pages/template-database-size.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ To have a smaller file size [DBCC SHRINKFILE](https://docs.microsoft.com/en-us/s
2121
use model;
2222
dbcc shrinkfile(modeldev, {size})
2323
```
24-
<sup><a href='/src/LocalDb/SqlBuilder.cs#L70-L73' title='Snippet source file'>snippet source</a> | <a href='#snippet-ShrinkModelDb' title='Start of snippet'>anchor</a></sup>
24+
<sup><a href='/src/LocalDb/SqlBuilder.cs#L84-L87' title='Snippet source file'>snippet source</a> | <a href='#snippet-ShrinkModelDb' title='Start of snippet'>anchor</a></sup>
2525
<!-- endSnippet -->
2626

2727

src/EfLocalDb.Tests/Tests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,19 @@ await ThrowsTask(() => database.SaveChangesAsync())
7878
.IgnoreStackTrace();
7979
}
8080

81+
[Test]
82+
public async Task TemplateHasReadCommittedSnapshot()
83+
{
84+
// Guards against accidental removal of the ALTER DATABASE statement that enables
85+
// READ_COMMITTED_SNAPSHOT on the template — required to avoid S/X-lock deadlocks
86+
// between parallel [SharedDbWithTransaction] tests on the same shared database.
87+
await using var database = await instance.Build();
88+
await using var command = database.Connection.CreateCommand();
89+
command.CommandText = "select is_read_committed_snapshot_on from sys.databases where name = db_name()";
90+
var enabled = (bool) (await command.ExecuteScalarAsync())!;
91+
True(enabled);
92+
}
93+
8194
[Test]
8295
public async Task RemoveData()
8396
{

src/LocalDb/SqlBuilder.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ create database [template] on
5050
execute sp_detach_db N'template', 'true';
5151
""";
5252

53+
// Database-level settings applied to the template before detach. Persisted in the
54+
// .mdf/.ldf files, so every database attached from the template inherits them.
55+
// auto_update_statistics off - avoids background stats updates causing
56+
// nondeterministic test timing.
57+
// read_committed_snapshot on - READ COMMITTED uses row versioning instead of
58+
// shared locks, preventing S/X-lock deadlocks
59+
// between parallel [SharedDbWithTransaction] tests
60+
// against the same shared database.
61+
public static string TemplateSettingsCommand =
62+
"""
63+
alter database [template] set auto_update_statistics off;
64+
alter database [template] set read_committed_snapshot on;
65+
""";
66+
5367
public static string DetachAndShrinkTemplateCommand =
5468
"""
5569
use [template];

src/LocalDb/SqlExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
static class SqlExtensions
22
{
3-
public static async Task ExecuteCommandAsync(this SqlConnection connection, string commandText)
3+
public static async Task ExecuteCommandAsync(this SqlConnection connection, params string[] commandTexts)
44
{
5-
commandText = commandText.Trim();
5+
var commandText = string.Join('\n', commandTexts).Trim();
66

77
try
88
{

src/LocalDb/Wrapper.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -260,10 +260,10 @@ async Task CreateAndDetachTemplate(
260260
await connection.ExecuteCommandAsync("checkpoint");
261261
}
262262

263-
await masterConnection.ExecuteCommandAsync("alter database [template] set auto_update_statistics off");
264-
265-
// Detach the template database after callback completes
266-
await masterConnection.ExecuteCommandAsync(SqlBuilder.DetachTemplateCommand);
263+
// Apply template settings then detach in a single batch.
264+
await masterConnection.ExecuteCommandAsync(
265+
SqlBuilder.TemplateSettingsCommand,
266+
SqlBuilder.DetachTemplateCommand);
267267
}
268268
}
269269
}
@@ -299,8 +299,9 @@ async Task Rebuild(DateTime timestamp, Func<SqlConnection, Task> buildTemplate,
299299
await connection.ExecuteCommandAsync("checkpoint");
300300
}
301301

302-
await masterConnection.ExecuteCommandAsync("alter database [template] set auto_update_statistics off");
303-
await masterConnection.ExecuteCommandAsync(SqlBuilder.DetachAndShrinkTemplateCommand);
302+
await masterConnection.ExecuteCommandAsync(
303+
SqlBuilder.TemplateSettingsCommand,
304+
SqlBuilder.DetachAndShrinkTemplateCommand);
304305

305306
File.SetCreationTime(DataFile, timestamp);
306307
}

0 commit comments

Comments
 (0)