Skip to content

Commit 6e3c93e

Browse files
committed
feat: basic db maintenance
1 parent 959566c commit 6e3c93e

4 files changed

Lines changed: 195 additions & 0 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
using System.Data;
2+
using System.Reflection;
3+
using Dapper;
4+
using WebApi.Scripts;
5+
6+
namespace WebApi.HostedServices;
7+
8+
/// <summary>
9+
/// Database maintenance service
10+
/// </summary>
11+
public class DbMaintenanceService : BackgroundService
12+
{
13+
private readonly ILogger<DbMaintenanceService> _logger;
14+
private readonly IDbConnection _dbConnection;
15+
16+
/// <summary>
17+
/// C'tor
18+
/// </summary>
19+
public DbMaintenanceService(
20+
ILogger<DbMaintenanceService> logger,
21+
IDbConnection dbConnection
22+
)
23+
{
24+
_logger = logger;
25+
_dbConnection = dbConnection;
26+
}
27+
28+
/// <inheritdoc />
29+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
30+
{
31+
await CreateMaintenanceTablesIfNeeded();
32+
33+
await foreach (var script in GetScriptsToRun().WithCancellation(stoppingToken))
34+
{
35+
var isSuccessful = await RunDbMaintenanceScript(script);
36+
37+
if (!isSuccessful)
38+
return;
39+
}
40+
}
41+
42+
private async Task<bool> RunDbMaintenanceScript(IDbMaintenanceScript script)
43+
{
44+
try
45+
{
46+
await script.Run(_dbConnection);
47+
48+
var scriptName = script.GetType().Name;
49+
50+
await _dbConnection.ExecuteAsync(
51+
"INSERT INTO DatabaseMaintenance (ScriptName, RanAt, IsSuccessful) VALUES (@scriptName, @ranAt, 1)",
52+
new
53+
{
54+
scriptName,
55+
ranAt = DateTimeOffset.UtcNow,
56+
});
57+
58+
_logger.LogInformation("Database maintenance script {ScriptName} ran successfully.", scriptName);
59+
60+
return true;
61+
}
62+
catch (Exception ex)
63+
{
64+
var scriptName = script.GetType().Name;
65+
66+
_logger.LogError(ex, "An error occurred while running database maintenance script {ScriptName}.", scriptName);
67+
68+
var errorDetails = new
69+
{
70+
version = 1,
71+
exception = GetExceptionDetails(ex),
72+
};
73+
74+
await _dbConnection.ExecuteAsync(
75+
"""
76+
INSERT INTO DatabaseMaintenance (ScriptName, RanAt, IsSuccessful, ErrorDetails)
77+
VALUES (@scriptName, @ranAt, 0, @errorDetails)
78+
""",
79+
new
80+
{
81+
scriptName,
82+
ranAt = DateTimeOffset.UtcNow,
83+
errorDetails = System.Text.Json.JsonSerializer.Serialize(errorDetails),
84+
});
85+
}
86+
87+
return false;
88+
}
89+
90+
private static object? GetExceptionDetails(Exception? ex)
91+
{
92+
if (ex is null)
93+
return null;
94+
95+
return new
96+
{
97+
exceptionMessage = ex.Message,
98+
stackTrace = ex.StackTrace,
99+
innerException = GetExceptionDetails(ex?.InnerException),
100+
};
101+
}
102+
103+
private async IAsyncEnumerable<IDbMaintenanceScript> GetScriptsToRun()
104+
{
105+
var allScripts = Assembly.GetExecutingAssembly().GetTypes()
106+
.Where(p => typeof(IDbMaintenanceScript).IsAssignableFrom(p) && !p.IsInterface)
107+
.Select(Activator.CreateInstance)
108+
.Cast<IDbMaintenanceScript>()
109+
.OrderBy(x => x.GetType().Name)
110+
.ToArray();
111+
112+
foreach (var script in allScripts)
113+
{
114+
var scriptName = script.GetType().Name;
115+
116+
var scriptHasRun = await _dbConnection.ExecuteScalarAsync<bool>(
117+
"SELECT EXISTS (SELECT 1 FROM DatabaseMaintenance WHERE ScriptName = @scriptName AND IsSuccessful = 1)",
118+
new { scriptName });
119+
120+
if (scriptHasRun)
121+
continue;
122+
123+
yield return script;
124+
}
125+
}
126+
127+
private async Task CreateMaintenanceTablesIfNeeded()
128+
{
129+
try
130+
{
131+
var tableExists = await _dbConnection.ExecuteScalarAsync<bool>(
132+
"SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'DatabaseMaintenance')");
133+
134+
if (tableExists)
135+
return;
136+
137+
const string sql = """
138+
CREATE TABLE DatabaseMaintenance
139+
(
140+
RunId INTEGER CONSTRAINT PK_DatabaseMaintenance PRIMARY KEY AUTOINCREMENT,
141+
ScriptName TEXT NOT NULL,
142+
RanAt TEXT NOT NULL,
143+
IsSuccessful INTEGER NOT NULL,
144+
ErrorDetails TEXT
145+
);
146+
""";
147+
148+
await _dbConnection.ExecuteAsync(sql);
149+
}
150+
catch (Exception ex)
151+
{
152+
_logger.LogCritical(ex, "An error occurred while creating database maintenance tables.");
153+
}
154+
}
155+
}

WebApi/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using SnooBrowser.Extensions.DependencyInjection;
1818
using Swashbuckle.AspNetCore.SwaggerUI;
1919
using WebApi.AuthHandlers;
20+
using WebApi.HostedServices;
2021
using WebApi.Middleware;
2122
using WebApi.Models.Swagger;
2223
using WebApi.Options;
@@ -162,6 +163,7 @@ static void ConfigureServices(IServiceCollection services, IConfiguration config
162163

163164
services.AddSingleton<GlobalMemoryCache>();
164165
services.AddSingleton<TemplateCache>();
166+
services.AddHostedService<DbMaintenanceService>();
165167
services.AddHostedService<BackgroundServiceWorker>();
166168
}
167169

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System.Data;
2+
3+
namespace WebApi.Scripts;
4+
5+
/// <summary>
6+
/// Database maintenance script
7+
/// </summary>
8+
public interface IDbMaintenanceScript
9+
{
10+
/// <summary>
11+
/// The script to run
12+
/// </summary>
13+
public Task Run(IDbConnection dbConnection);
14+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Data;
2+
using Dapper;
3+
4+
namespace WebApi.Scripts;
5+
6+
/// <summary>
7+
/// Set performance options
8+
/// </summary>
9+
public class Script_2024_03_08_01_SetPerformanceOptions : IDbMaintenanceScript
10+
{
11+
/// <inheritdoc />
12+
public async Task Run(IDbConnection dbConnection)
13+
{
14+
var journalMode = await dbConnection.ExecuteScalarAsync<string?>("PRAGMA journal_mode");
15+
16+
if (journalMode is null || !journalMode.Equals("WAL", StringComparison.OrdinalIgnoreCase))
17+
await dbConnection.ExecuteAsync("PRAGMA journal_mode = WAL");
18+
19+
var synchronous = await dbConnection.ExecuteScalarAsync<string?>("PRAGMA synchronous");
20+
21+
if (synchronous is null || !synchronous.Equals("NORMAL", StringComparison.OrdinalIgnoreCase))
22+
await dbConnection.ExecuteAsync("PRAGMA synchronous = NORMAL");
23+
}
24+
}

0 commit comments

Comments
 (0)