Skip to content

Commit 2449e65

Browse files
ci: add rich SEO-optimized release body to deploy workflow
GitHub Release description now includes the exact exception strings developers search for (SQLITE_BUSY, database is locked, SQLITE_BUSY_SNAPSHOT), installation snippet, problems table, quick-setup code, benchmark results, and links to all documentation pages.
1 parent a15a801 commit 2449e65

1 file changed

Lines changed: 124 additions & 2 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
VERSION=$(grep -oP '(?<=<Version>).*?(?=</Version>)' EntityFrameworkCore.Sqlite.Concurrency/EFCore.Sqlite.Concurrency.csproj)
4040
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
4141
echo "Detected version: $VERSION"
42-
42+
4343
if [[ $VERSION == *"-"* ]] || [[ "${{ github.event.inputs.environment }}" == "beta" ]]; then
4444
echo "IS_PRERELEASE=true" >> $GITHUB_OUTPUT
4545
else
@@ -60,12 +60,134 @@ jobs:
6060
uses: softprops/action-gh-release@v2
6161
with:
6262
tag_name: v${{ steps.get_version.outputs.VERSION }}
63-
name: Release v${{ steps.get_version.outputs.VERSION }}
63+
name: "EntityFrameworkCore.Sqlite.Concurrency v${{ steps.get_version.outputs.VERSION }}"
6464
draft: false
6565
prerelease: ${{ steps.get_version.outputs.IS_PRERELEASE == 'true' }}
6666
files: |
6767
out/*.nupkg
6868
out/*.snupkg
69+
body: |
70+
## Fix `SQLite Error 5: 'database is locked'` in EF Core
71+
72+
If your application throws any of these exceptions, this package is the fix:
73+
74+
```
75+
Microsoft.Data.Sqlite.SqliteException: SQLite Error 5: 'database is locked'
76+
Microsoft.Data.Sqlite.SqliteException: SQLite Error 5: 'database is locked' (SQLITE_BUSY_SNAPSHOT)
77+
InvalidOperationException: A second operation was started on this context instance
78+
```
79+
80+
One line change — no other modifications to your application code:
81+
82+
```csharp
83+
// Before
84+
options.UseSqlite("Data Source=app.db");
85+
86+
// After — eliminates SQLITE_BUSY, serializes writes, enables WAL-mode parallel reads
87+
options.UseSqliteWithConcurrency("Data Source=app.db");
88+
```
89+
90+
---
91+
92+
## What's New in v${{ steps.get_version.outputs.VERSION }}
93+
94+
### Channel-Based Write Queue
95+
96+
Replaces the per-database `SemaphoreSlim(1,1)` with a `Channel<IWriteRequest>`-based write queue backed by a single background writer.
97+
98+
Under concurrent write load, `SemaphoreSlim.Release()` wakes all N waiters simultaneously — N-1 immediately go back to sleep. This thundering-herd pattern causes CPU spikes and unfair scheduling. A `Channel<T>` parks callers in FIFO order and drains them one at a time. No wakeup storm. No API change required.
99+
100+
- New `SqliteWriteQueue` internal class owns the channel and the background writer loop
101+
- New `WriteQueueCapacity` option: `null` = unbounded (default), integer = bounded with `BoundedChannelFullMode.Wait` for back-pressure
102+
- Zero breaking changes
103+
104+
### Bug Fixes
105+
106+
- `ThreadSafeSqliteContext.ExecuteWriteAsync` was ignoring registered options (always using defaults). Fixed via interceptor lookup.
107+
- `BulkInsertSafeAsync` used `Skip/Take` in a loop — O(n^2) complexity. Replaced with `Chunk(1000)`.
108+
- `BulkInsertSafeAsync` did not call `ChangeTracker.Clear()` between batches — memory grew linearly with entity count. Now cleared after each batch.
109+
110+
### netstandard 2.0 Support
111+
112+
The connection and queue layer now targets `netstandard2.0`. EF Core-dependent APIs remain `net10.0`-only.
113+
114+
### Real BenchmarkDotNet Results
115+
116+
Measured on .NET 10.0.2 / Windows 11 / Intel i7-13700K:
117+
118+
| Operation | Standard EF Core | This Package | Ratio |
119+
|---|---|---|---|
120+
| Bulk insert — 1,000 entities | 4,500 ms | **28 ms** | ~161x faster |
121+
| Concurrent writes — 50 tasks (`Task.WhenAll`) | throws `SQLITE_BUSY` | completes cleanly | N/A |
122+
123+
---
124+
125+
## Installation
126+
127+
```bash
128+
dotnet add package EntityFrameworkCore.Sqlite.Concurrency
129+
```
130+
131+
Or search **EntityFrameworkCore.Sqlite.Concurrency** on [NuGet.org](https://www.nuget.org/packages/EntityFrameworkCore.Sqlite.Concurrency).
132+
133+
---
134+
135+
## Problems This Solves
136+
137+
| Error | Cause | Fix |
138+
|---|---|---|
139+
| `SQLite Error 5: 'database is locked'` (SQLITE_BUSY) | Multiple concurrent writers | Channel-based write queue — one writer at a time, all others wait in FIFO order |
140+
| `SQLite Error 5: 'database is locked'` (SQLITE_BUSY_SNAPSHOT, code 517) | WAL read snapshot stale mid-transaction | `BEGIN IMMEDIATE` upgrade + full operation restart |
141+
| `A second operation was started on this context instance` | Shared `DbContext` across concurrent tasks | `IDbContextFactory<T>` registration |
142+
| Slow bulk inserts | One `SaveChanges` per entity | `BulkInsertOptimizedAsync` — batched writes, ChangeTracker cleared per batch |
143+
144+
---
145+
146+
## Quick Setup
147+
148+
**ASP.NET Core / request-scoped:**
149+
```csharp
150+
builder.Services.AddConcurrentSqliteDbContext<AppDbContext>("Data Source=app.db");
151+
```
152+
153+
**Background services / `Task.WhenAll` / concurrent workloads:**
154+
```csharp
155+
builder.Services.AddConcurrentSqliteDbContextFactory<AppDbContext>("Data Source=app.db");
156+
157+
// Each concurrent task gets its own context — writes are serialized automatically
158+
var tasks = ids.Select(async id =>
159+
{
160+
await using var db = factory.CreateDbContext();
161+
db.Items.Add(new Item { Id = id });
162+
await db.SaveChangesAsync(); // never throws SQLITE_BUSY
163+
});
164+
await Task.WhenAll(tasks);
165+
```
166+
167+
**Bulk import:**
168+
```csharp
169+
await context.BulkInsertOptimizedAsync(records); // 161x faster than SaveChanges-per-entity
170+
```
171+
172+
---
173+
174+
## Documentation
175+
176+
- [Troubleshooting SQLITE_BUSY errors](https://github.com/CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency/blob/main/docs/troubleshooting-sqlite-busy.md)
177+
- [Concurrent EF Core patterns](https://github.com/CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency/blob/main/docs/concurrent-efcore-patterns.md)
178+
- [Performance and WAL tuning guide](https://github.com/CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency/blob/main/docs/performance-guide.md)
179+
- [Migration guide from plain UseSqlite](https://github.com/CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency/blob/main/docs/migration-guide.md)
180+
- [Full README](https://github.com/CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency/blob/main/README.md)
181+
- [CHANGELOG](https://github.com/CornerstoneCode/EntityFrameworkCore.Sqlite.Concurrency/blob/main/CHANGELOG.md)
182+
183+
---
184+
185+
## Requirements
186+
187+
- .NET 10.0+ (for EF Core APIs) or netstandard 2.0+ (for connection layer only)
188+
- Entity Framework Core 10.0+
189+
- Microsoft.Data.Sqlite 10.0+
190+
69191
env:
70192
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
71193

0 commit comments

Comments
 (0)