feat: migrate runtime to postgresql

This commit is contained in:
2026-04-29 18:47:12 +08:00
parent 49d893f29b
commit 8132466c5d
17 changed files with 2347 additions and 306 deletions
@@ -11,11 +11,11 @@ using LiveRecorder.Application.Models.RecordTasks;
using LiveRecorder.Application.Services;
using LiveRecorder.Domain.Enums;
using LiveRecorder.Infrastructure.Persistence;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Npgsql;
namespace LiveRecorder.Infrastructure.Services;
@@ -637,13 +637,13 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService, ILiveR
}
if (exception is DbUpdateException dbUpdateException &&
IsSqliteLockException(dbUpdateException))
IsTransientDatabaseException(dbUpdateException))
{
return true;
}
if (exception is SqliteException sqliteException &&
IsSqliteLockException(sqliteException))
if (exception is NpgsqlException npgsqlException &&
npgsqlException.IsTransient)
{
return true;
}
@@ -691,9 +691,9 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService, ILiveR
private static string BuildExceptionEmailKey(string scope, Exception exception, Guid? liveRoomId = null)
{
if (IsSqliteStorageFullException(exception))
if (IsDatabaseStorageFullException(exception))
{
return $"{scope}:sqlite-storage-full";
return $"{scope}:database-storage-full";
}
var root = exception.GetBaseException();
@@ -701,16 +701,21 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService, ILiveR
return $"{scope}:{liveRoomId?.ToString() ?? "global"}:{root.GetType().FullName}:{message}";
}
private static bool IsSqliteStorageFullException(Exception exception)
private static bool IsDatabaseStorageFullException(Exception exception)
{
if (exception is SqliteException sqliteException &&
(sqliteException.SqliteErrorCode == 13 ||
sqliteException.Message.Contains("database or disk is full", StringComparison.OrdinalIgnoreCase)))
if (exception is PostgresException postgresException &&
postgresException.SqlState == PostgresErrorCodes.DiskFull)
{
return true;
}
return exception.InnerException is not null && IsSqliteStorageFullException(exception.InnerException);
if (exception is IOException ioException &&
ioException.Message.Contains("no space left on device", StringComparison.OrdinalIgnoreCase))
{
return true;
}
return exception.InnerException is not null && IsDatabaseStorageFullException(exception.InnerException);
}
private static async Task UpdateAutoStartDecisionAsync(
@@ -738,21 +743,15 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService, ILiveR
await dbContext.SaveChangesAsync(cancellationToken);
return;
}
catch (DbUpdateException ex) when (attempt < 5 && IsSqliteLockException(ex))
catch (DbUpdateException ex) when (attempt < 5 && IsTransientDatabaseException(ex))
{
await Task.Delay(TimeSpan.FromMilliseconds(300 * Math.Pow(2, attempt - 1)), cancellationToken);
}
}
}
private static bool IsSqliteLockException(DbUpdateException exception) =>
exception.InnerException is SqliteException sqliteException &&
IsSqliteLockException(sqliteException);
private static bool IsSqliteLockException(SqliteException exception) =>
exception.SqliteErrorCode is 5 or 6 ||
exception.Message.Contains("database is locked", StringComparison.OrdinalIgnoreCase) ||
exception.Message.Contains("database table is locked", StringComparison.OrdinalIgnoreCase);
private static bool IsTransientDatabaseException(DbUpdateException exception) =>
exception.InnerException is NpgsqlException npgsqlException && npgsqlException.IsTransient;
private static string? Truncate(string? value, int maxLength)
{