Skip to content

Commit cec2424

Browse files
authored
Merge pull request #966 from Chris0Jeky/fix/perf-14-dbcontext-resilience
Add DbContext connection resilience (timeout and retry)
2 parents fe091a1 + 01d07d7 commit cec2424

5 files changed

Lines changed: 98 additions & 1 deletion

File tree

backend/src/Taskdeck.Api/appsettings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@
116116
"EncryptionKey": ""
117117
},
118118
"AllowedHosts": "*",
119+
"Database": {
120+
"CommandTimeoutSeconds": 30
121+
},
119122
"ConnectionStrings": {
120123
"DefaultConnection": "Data Source=taskdeck.db"
121124
},
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace Taskdeck.Application.Services;
4+
5+
/// <summary>
6+
/// Configuration for DbContext connection resilience.
7+
/// Bound from appsettings.json "Database" section.
8+
/// </summary>
9+
public sealed class DatabaseSettings
10+
{
11+
/// <summary>
12+
/// Command timeout in seconds for database operations.
13+
/// Applied to <c>SqliteDbContextOptionsBuilder.CommandTimeout</c>.
14+
/// This affects all EF Core commands including migrations
15+
/// (<c>Database.Migrate()</c>). Avoid setting very low values (e.g. 1s)
16+
/// if schema migrations are expected.
17+
/// </summary>
18+
[Range(1, 300, ErrorMessage = "CommandTimeoutSeconds must be between 1 and 300.")]
19+
public int CommandTimeoutSeconds { get; set; } = 30;
20+
}

backend/src/Taskdeck.Infrastructure/DependencyInjection.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,24 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
2020
var connectionString = configuration.GetConnectionString("DefaultConnection")
2121
?? "Data Source=taskdeck.db";
2222

23+
var databaseSettings = configuration.GetSection("Database").Get<DatabaseSettings>()
24+
?? new DatabaseSettings();
25+
26+
// Enforce validation for all host modes (API, CLI, MCP).
27+
// ValidateOnStart causes an exception at startup if CommandTimeoutSeconds
28+
// is out of the [1, 300] range, regardless of which host runs AddInfrastructure.
29+
services.AddOptions<DatabaseSettings>()
30+
.Bind(configuration.GetSection("Database"))
31+
.ValidateDataAnnotations()
32+
.ValidateOnStart();
33+
2334
services.AddDbContext<TaskdeckDbContext>(options =>
24-
options.UseSqlite(connectionString));
35+
options.UseSqlite(connectionString, sqliteOptions =>
36+
{
37+
// Apply command timeout from configuration (default: 30s).
38+
// This applies to all EF Core commands including Database.Migrate().
39+
sqliteOptions.CommandTimeout(databaseSettings.CommandTimeoutSeconds);
40+
}));
2541

2642
services.AddScoped<IBoardRepository, BoardRepository>();
2743
services.AddScoped<IColumnRepository, ColumnRepository>();

backend/tests/Taskdeck.Api.Tests/Validation/OptionsValidationTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,54 @@ public void SecurityHeadersSettings_DefaultValues_PassValidation()
496496
Assert.True(isValid);
497497
}
498498

499+
// ── DatabaseSettings data annotation validation ───────────────────
500+
501+
[Fact]
502+
public void DatabaseSettings_DefaultValues_PassValidation()
503+
{
504+
var settings = new DatabaseSettings();
505+
506+
var context = new System.ComponentModel.DataAnnotations.ValidationContext(settings);
507+
var results = new List<System.ComponentModel.DataAnnotations.ValidationResult>();
508+
var isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateObject(
509+
settings, context, results, validateAllProperties: true);
510+
511+
Assert.True(isValid);
512+
}
513+
514+
[Theory]
515+
[InlineData(0)]
516+
[InlineData(-1)]
517+
[InlineData(301)]
518+
public void DatabaseSettings_CommandTimeoutSeconds_RejectsOutOfRange(int value)
519+
{
520+
var settings = new DatabaseSettings { CommandTimeoutSeconds = value };
521+
522+
var context = new System.ComponentModel.DataAnnotations.ValidationContext(settings);
523+
var results = new List<System.ComponentModel.DataAnnotations.ValidationResult>();
524+
var isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateObject(
525+
settings, context, results, validateAllProperties: true);
526+
527+
Assert.False(isValid);
528+
Assert.Contains(results, r => r.MemberNames.Contains(nameof(DatabaseSettings.CommandTimeoutSeconds)));
529+
}
530+
531+
[Theory]
532+
[InlineData(1)]
533+
[InlineData(30)]
534+
[InlineData(300)]
535+
public void DatabaseSettings_CommandTimeoutSeconds_AcceptsValidValues(int value)
536+
{
537+
var settings = new DatabaseSettings { CommandTimeoutSeconds = value };
538+
539+
var context = new System.ComponentModel.DataAnnotations.ValidationContext(settings);
540+
var results = new List<System.ComponentModel.DataAnnotations.ValidationResult>();
541+
var isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateObject(
542+
settings, context, results, validateAllProperties: true);
543+
544+
Assert.True(isValid);
545+
}
546+
499547
// ── AuditRetentionSettings ────────────────────────────────────────
500548

501549
[Fact]

docs/platform/CONFIGURATION_REFERENCE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Source files used to build this reference:
5454
- [`AuditRetention`](#auditretention)
5555
- [Persistence and first run](#persistence-and-first-run)
5656
- [`ConnectionStrings`](#connectionstrings)
57+
- [`Database`](#database)
5758
- [`ExportImport`](#exportimport)
5859
- [`FirstRun`](#firstrun)
5960
- [`DevelopmentSandbox`](#developmentsandbox)
@@ -492,6 +493,15 @@ DbContext) and
492493
| --- | --- | --- | --- | --- |
493494
| `ConnectionStrings:DefaultConnection` | `string` | `Data Source=taskdeck.db` | SQLite connection string. When `FirstRun:ResolveAppDataDbPath` is true and the path is relative, first-run resolves it into the OS LocalAppData directory (`%LOCALAPPDATA%/Taskdeck` on Windows, XDG equivalent on Linux). | Yes (but a default is always supplied) |
494495

496+
### `Database`
497+
498+
Consumed by `Taskdeck.Infrastructure.DependencyInjection.AddInfrastructure`.
499+
Backs `DatabaseSettings` (`Taskdeck.Application.Services.DatabaseSettings`).
500+
501+
| Key | Type | Default | Description | Required? |
502+
| --- | --- | --- | --- | --- |
503+
| `Database:CommandTimeoutSeconds` | `int` | `30` | Command timeout in seconds for database operations. Valid range: 1--300. Applies to all EF Core commands including `Database.Migrate()` -- avoid very low values if schema migrations are expected. | No |
504+
495505
### `ExportImport`
496506

497507
Consumed directly by `SettingsRegistration.cs`. Backs

0 commit comments

Comments
 (0)