Skip to content

Commit 87c2176

Browse files
parallel enhancements
Here's a summary of every change made: New files File Purpose src/SqliteErrorCodes.cs Internal constants and classifiers for SQLITE_BUSY, SQLITE_BUSY_SNAPSHOT (517), SQLITE_BUSY_RECOVERY, SQLITE_LOCKED, etc. src/Models/SqliteSynchronousMode.cs Enum for PRAGMA synchronous (Off/Normal/Full/Extra) with full durability trade-off documentation src/Models/WalCheckpointStatus.cs Record returned by GetWalCheckpointStatusAsync — exposes busy flag, total frames, checkpointed frames, and progress % Modified files SqliteConcurrencyOptions.cs Added SynchronousMode (default Normal), UpgradeTransactionsToImmediate (default true), LoggerFactory (excluded from equality) Added Validate() with bounds checks on all numeric properties Updated Equals/GetHashCode to include new behavior-affecting properties SqliteConcurrencyInterceptor.cs Creates ILogger<SqliteConcurrencyInterceptor> from options.LoggerFactory Logs SQLITE_BUSY_SNAPSHOT (Warning), SQLITE_BUSY (Warning), and SQLITE_LOCKED (Error) in CommandFailed/CommandFailedAsync Logs BEGIN IMMEDIATE upgrades at Debug level Respects UpgradeTransactionsToImmediate = false; also now catches BEGIN DEFERRED SqliteConnectionEnhancer.cs Uses options.SynchronousMode.ToString().ToUpperInvariant() instead of hardcoded NORMAL Added inline comments on every pragma explaining the reason, trade-off, and safe range Added GetWalCheckpointStatusAsync() — runs PRAGMA wal_checkpoint(PASSIVE) and returns WalCheckpointStatus SqliteConcurrencyExtensions.cs Calls options.Validate() on startup Retry now catches via SqliteErrorCodes.IsAnyBusy() (covers all extended codes); SQLITE_LOCKED propagates immediately Backoff replaced with exponential + full jitter: sleep in [baseDelay, 2×baseDelay] ThreadSafeSqliteContext.cs Same SqliteErrorCodes-based retry; SQLITE_BUSY_SNAPSHOT restarts the full operation lambda (correct semantics — stale data is re-queried) Jitter added to backoff TimeoutException message now identifies whether it was a snapshot error and includes the extended error code SqliteConcurrencyServiceCollectionExtensions.cs Resolves ILoggerFactory from the DI container and injects it into options automatically doc/QUICKSTART.md Removed all UseWriteQueue references Replaced the incorrect options table with the real six options, each with accurate defaults and descriptions
1 parent 9fb4731 commit 87c2176

13 files changed

Lines changed: 689 additions & 109 deletions

EntityFrameworkCore.Sqlite.Concurrency/EFCore.Sqlite.Concurrency.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,9 @@ Get started in one line. Stop compromising on SQLite reliability and speed.
9393
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
9494
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.2" />
9595
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
96-
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
96+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
9797
<!-- SourceLink for debugging support -->
98-
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
98+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.201" PrivateAssets="All" />
9999
</ItemGroup>
100100

101101
<!-- Optional Dependencies (Conditional) -->

EntityFrameworkCore.Sqlite.Concurrency/doc/QUICKSTART.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ builder.Services.AddDbContext<BlogDbContext>(options =>
5252
"Data Source=blog.db",
5353
sqliteOptions =>
5454
{
55-
sqliteOptions.UseWriteQueue = true; // Enable write serialization
5655
sqliteOptions.BusyTimeout = TimeSpan.FromSeconds(30);
5756
sqliteOptions.MaxRetryAttempts = 5;
5857
}));
@@ -216,8 +215,7 @@ public class TaskProcessor
216215
```csharp
217216
// Create contexts manually when needed
218217
var dbContext = ThreadSafeFactory.CreateContext<BlogDbContext>(
219-
"Data Source=blog.db",
220-
options => options.UseWriteQueue = true);
218+
"Data Source=blog.db");
221219

222220
// Use it
223221
await dbContext.Posts.AddAsync(new Post { Title = "Hello World" });
@@ -254,11 +252,12 @@ public async Task UpdatePostWithRetryAsync(int postId, string newContent)
254252

255253
| Option | Default | Description |
256254
|--------|---------|-------------|
257-
| `UseWriteQueue` | `true` | Automatically queue write operations |
258-
| `BusyTimeout` | 30 seconds | How long to wait if database is busy |
259-
| `MaxRetryAttempts` | 3 | Number of retries for busy errors |
260-
| `CommandTimeout` | 300 seconds | SQL command timeout |
261-
| `EnableWalCheckpointManagement` | `true` | Automatically manage WAL checkpoints |
255+
| `BusyTimeout` | 30 seconds | Per-connection `PRAGMA busy_timeout`. First layer of busy handling; SQLite retries lock acquisition internally for up to this duration. |
256+
| `MaxRetryAttempts` | 3 | Application-level retry attempts for `SQLITE_BUSY*` errors, with exponential backoff and jitter. |
257+
| `CommandTimeout` | 300 seconds | EF Core SQL command timeout in seconds. |
258+
| `WalAutoCheckpoint` | 1000 pages | WAL auto-checkpoint interval (`PRAGMA wal_autocheckpoint`). Each page is 4 096 bytes by default (~4 MB). Set to `0` to disable. |
259+
| `SynchronousMode` | `Normal` | Durability vs. performance trade-off (`PRAGMA synchronous`). `Normal` is recommended for WAL mode: safe against application crashes; a power loss or OS crash may roll back the last commit(s) not yet checkpointed. Use `Full` or `Extra` for stronger durability guarantees. |
260+
| `UpgradeTransactionsToImmediate` | `true` | Rewrites `BEGIN`/`BEGIN TRANSACTION` to `BEGIN IMMEDIATE` to prevent `SQLITE_BUSY_SNAPSHOT` mid-transaction. Disable only if you manage write transactions explicitly yourself. |
262261

263262
## Best Practices
264263

EntityFrameworkCore.Sqlite.Concurrency/doc/v10_0_0.md

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ options.UseSqlite("Data Source=app.db");
1919
// With this:
2020
options.UseSqliteWithConcurrency("Data Source=app.db");
2121
```
22-
Guaranteed 100% write reliability and up to 10x faster bulk operations.
22+
Eliminates write contention errors and provides up to 10x faster bulk operations.
2323

2424
---
2525

@@ -70,7 +70,7 @@ Next, explore high-performance bulk inserts or fine-tune the configuration.
7070
| **Mixed Read/Write Workload** | ~15.3 seconds | ~3.8 seconds | **4.0x faster** |
7171
| **Memory Usage (100k operations)** | ~425 MB | ~285 MB | **33% less memory** |
7272

73-
*Benchmark environment: .NET 10, Windows 11, Intel i7-13700K, 32GB RAM*
73+
**Benchmark environment:** .NET 10, Windows 11, Intel i7-13700K, 32GB RAM*
7474

7575
---
7676

@@ -82,13 +82,7 @@ public async Task PerformDataMigrationAsync(List<LegacyData> legacyRecords)
8282
{
8383
var modernRecords = legacyRecords.Select(ConvertToModernFormat);
8484

85-
await _context.BulkInsertSafeAsync(modernRecords, new BulkConfig
86-
{
87-
BatchSize = 5000,
88-
PreserveInsertOrder = true,
89-
EnableStreaming = true,
90-
UseOptimalTransactionSize = true
91-
});
85+
await _context.BulkInsertSafeAsyncmodernRecords);
9286
}
9387
```
9488

@@ -114,8 +108,7 @@ public async Task<TResult> ExecuteHighPerformanceOperationAsync<TResult>(
114108
Func<DbContext, Task<TResult>> operation)
115109
{
116110
using var context = ThreadSafeFactory.CreateContext<AppDbContext>(
117-
"Data Source=app.db",
118-
options => options.EnablePerformanceOptimizations = true);
111+
"Data Source=app.db");
119112

120113
return await context.ExecuteWithRetryAsync(operation, maxRetries: 2);
121114
}
@@ -136,7 +129,6 @@ services.AddDbContext<AppDbContext>(options =>
136129
concurrencyOptions.BusyTimeout = TimeSpan.FromSeconds(30);
137130
concurrencyOptions.MaxRetryAttempts = 3; // Performance-focused retry logic
138131
concurrencyOptions.CommandTimeout = 180; // 3-minute timeout for large operations
139-
concurrencyOptions.EnablePerformanceOptimizations = true; // Additional speed boosts
140132
}));
141133
```
142134

EntityFrameworkCore.Sqlite.Concurrency/packages.lock.json

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@
3030
},
3131
"Microsoft.Extensions.DependencyInjection": {
3232
"type": "Direct",
33-
"requested": "[10.0.2, )",
34-
"resolved": "10.0.2",
35-
"contentHash": "J/Zmp6fY93JbaiZ11ckWvcyxMPjD6XVwIHQXBjryTBgn7O6O20HYg9uVLFcZlNfgH78MnreE/7EH+hjfzn7VyA==",
33+
"requested": "[10.0.5, )",
34+
"resolved": "10.0.5",
35+
"contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==",
3636
"dependencies": {
37-
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.2"
37+
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5"
3838
}
3939
},
4040
"Microsoft.Extensions.Logging.Abstractions": {
@@ -48,18 +48,22 @@
4848
},
4949
"Microsoft.SourceLink.GitHub": {
5050
"type": "Direct",
51-
"requested": "[8.0.0, )",
52-
"resolved": "8.0.0",
53-
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
51+
"requested": "[10.0.201, )",
52+
"resolved": "10.0.201",
53+
"contentHash": "qxYAmO4ktzd9L+HMdnqWucxpu7bI9undPyACXOMqPyhaiMtbpbYL/n0ACyWIJlbyEJrXFwxiOaBOSasLtDvsCg==",
5454
"dependencies": {
55-
"Microsoft.Build.Tasks.Git": "8.0.0",
56-
"Microsoft.SourceLink.Common": "8.0.0"
55+
"Microsoft.Build.Tasks.Git": "10.0.201",
56+
"Microsoft.SourceLink.Common": "10.0.201",
57+
"System.IO.Hashing": "10.0.5"
5758
}
5859
},
5960
"Microsoft.Build.Tasks.Git": {
6061
"type": "Transitive",
61-
"resolved": "8.0.0",
62-
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
62+
"resolved": "10.0.201",
63+
"contentHash": "DMYBnrFZvLnBKn14VavEuuIr31CY6YY2i2L9P8DorS/Qp6ifRR8ZPLdJCFLFfjikNq8DykbYyLd/RP6lSqHcWw==",
64+
"dependencies": {
65+
"System.IO.Hashing": "10.0.5"
66+
}
6367
},
6468
"Microsoft.Data.Sqlite.Core": {
6569
"type": "Transitive",
@@ -145,8 +149,8 @@
145149
},
146150
"Microsoft.Extensions.DependencyInjection.Abstractions": {
147151
"type": "Transitive",
148-
"resolved": "10.0.2",
149-
"contentHash": "zOIurr59+kUf9vNcsUkCvKWZv+fPosUZXURZesYkJCvl0EzTc9F7maAO4Cd2WEV7ZJJ0AZrFQvuH6Npph9wdBw=="
152+
"resolved": "10.0.5",
153+
"contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA=="
150154
},
151155
"Microsoft.Extensions.DependencyModel": {
152156
"type": "Transitive",
@@ -179,8 +183,8 @@
179183
},
180184
"Microsoft.SourceLink.Common": {
181185
"type": "Transitive",
182-
"resolved": "8.0.0",
183-
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
186+
"resolved": "10.0.201",
187+
"contentHash": "QbBYhkjgL6rCnBfDbzsAJLlsad13TlBHqYCFDIw56OO2g6ix+9RsmY8uxiQGdWwFKbZXaXyAA6jDCzFYVGCZDw=="
184188
},
185189
"SQLitePCLRaw.bundle_e_sqlite3": {
186190
"type": "Transitive",
@@ -208,6 +212,11 @@
208212
"dependencies": {
209213
"SQLitePCLRaw.core": "2.1.11"
210214
}
215+
},
216+
"System.IO.Hashing": {
217+
"type": "Transitive",
218+
"resolved": "10.0.5",
219+
"contentHash": "8IBJWcCT9+e4Bmevm4T7+fQEiAh133KGiz4oiVTgJckd3Q76OFdR1falgn9lpz7+C4HJvogCDJeAa2QmvbeVtg=="
211220
}
212221
}
213222
}

EntityFrameworkCore.Sqlite.Concurrency/src/ExtensionMethods/SqliteConcurrencyServiceCollectionExtensions.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using EntityFrameworkCore.Sqlite.Concurrency.Models;
22
using Microsoft.EntityFrameworkCore;
33
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Logging;
45

56
namespace EntityFrameworkCore.Sqlite.Concurrency.ExtensionMethods;
67

@@ -18,6 +19,12 @@ public static class SqliteConcurrencyServiceCollectionExtensions
1819
/// <param name="configure">An optional action to configure concurrency options.</param>
1920
/// <param name="contextLifetime">The lifetime of the DbContext.</param>
2021
/// <returns>The service collection.</returns>
22+
/// <remarks>
23+
/// This overload automatically resolves <see cref="ILoggerFactory"/> from the DI
24+
/// container and injects it into the concurrency options so that <c>SQLITE_BUSY*</c>
25+
/// events and <c>BEGIN IMMEDIATE</c> upgrades are logged through the application's
26+
/// normal logging pipeline.
27+
/// </remarks>
2128
public static IServiceCollection AddConcurrentSqliteDbContext<TContext>(
2229
this IServiceCollection services,
2330
string connectionString,
@@ -27,9 +34,17 @@ public static IServiceCollection AddConcurrentSqliteDbContext<TContext>(
2734
{
2835
services.AddDbContext<TContext>((provider, options) =>
2936
{
30-
options.UseSqliteWithConcurrency(connectionString, configure);
37+
options.UseSqliteWithConcurrency(connectionString, o =>
38+
{
39+
configure?.Invoke(o);
40+
41+
// Inject the singleton ILoggerFactory so the interceptor can emit
42+
// structured logs without the caller having to wire it up manually.
43+
if (o.LoggerFactory is null)
44+
o.LoggerFactory = provider.GetService<ILoggerFactory>();
45+
});
3146
}, contextLifetime);
32-
47+
3348
return services;
3449
}
35-
}
50+
}

0 commit comments

Comments
 (0)