feat: migrate runtime to postgresql
This commit is contained in:
@@ -848,21 +848,21 @@ public sealed partial class FfmpegService
|
||||
var finalStatus = (int)recordTask.Status;
|
||||
|
||||
// Multiple background paths can reconcile the same segment after ffmpeg exits.
|
||||
// Use SQLite's atomic upsert instead of EF Add-or-Update to avoid RecordTaskId
|
||||
// unique constraint races between scoped DbContext instances.
|
||||
// Use the database's atomic upsert instead of EF Add-or-Update to avoid
|
||||
// RecordTaskId unique constraint races between scoped DbContext instances.
|
||||
await dbContext.Database.ExecuteSqlInterpolatedAsync($"""
|
||||
INSERT INTO RecordResults
|
||||
(Id, RecordTaskId, FilePath, FileSizeBytes, DurationSeconds, DanmakuFilePath, DanmakuMessageCount, FinalStatus, ErrorMessage, CreatedAt)
|
||||
INSERT INTO "RecordResults"
|
||||
("Id", "RecordTaskId", "FilePath", "FileSizeBytes", "DurationSeconds", "DanmakuFilePath", "DanmakuMessageCount", "FinalStatus", "ErrorMessage", "CreatedAt")
|
||||
VALUES
|
||||
({resultId}, {recordTask.Id}, {effectiveOutputPath}, {fileSize}, {durationSeconds}, {danmakuFilePath}, {normalizedDanmakuCount}, {finalStatus}, {recordTask.ErrorMessage}, {endedAt})
|
||||
ON CONFLICT(RecordTaskId) DO UPDATE SET
|
||||
FilePath = excluded.FilePath,
|
||||
FileSizeBytes = excluded.FileSizeBytes,
|
||||
DurationSeconds = excluded.DurationSeconds,
|
||||
DanmakuFilePath = excluded.DanmakuFilePath,
|
||||
DanmakuMessageCount = excluded.DanmakuMessageCount,
|
||||
FinalStatus = excluded.FinalStatus,
|
||||
ErrorMessage = excluded.ErrorMessage;
|
||||
ON CONFLICT("RecordTaskId") DO UPDATE SET
|
||||
"FilePath" = excluded."FilePath",
|
||||
"FileSizeBytes" = excluded."FileSizeBytes",
|
||||
"DurationSeconds" = excluded."DurationSeconds",
|
||||
"DanmakuFilePath" = excluded."DanmakuFilePath",
|
||||
"DanmakuMessageCount" = excluded."DanmakuMessageCount",
|
||||
"FinalStatus" = excluded."FinalStatus",
|
||||
"ErrorMessage" = excluded."ErrorMessage";
|
||||
""", cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user