From 9b99cd7e985a54c374eeb30d1d420310172d5894 Mon Sep 17 00:00:00 2001 From: nanxun Date: Thu, 30 Apr 2026 16:05:59 +0800 Subject: [PATCH] fix: retry transient postgres polling saves --- .../LiveRoomPollingBackgroundService.cs | 31 +++++++++++++++++-- src/LiveRecorder.WebApi/Program.cs | 28 +++++++++++++++-- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/LiveRecorder.Infrastructure/Services/LiveRoomPollingBackgroundService.cs b/src/LiveRecorder.Infrastructure/Services/LiveRoomPollingBackgroundService.cs index e3fb505..ac0cb3d 100644 --- a/src/LiveRecorder.Infrastructure/Services/LiveRoomPollingBackgroundService.cs +++ b/src/LiveRecorder.Infrastructure/Services/LiveRoomPollingBackgroundService.cs @@ -743,15 +743,40 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService, ILiveR await dbContext.SaveChangesAsync(cancellationToken); return; } - catch (DbUpdateException ex) when (attempt < 5 && IsTransientDatabaseException(ex)) + catch (Exception ex) when (attempt < 5 && IsTransientDatabaseException(ex)) { await Task.Delay(TimeSpan.FromMilliseconds(300 * Math.Pow(2, attempt - 1)), cancellationToken); } } } - private static bool IsTransientDatabaseException(DbUpdateException exception) => - exception.InnerException is NpgsqlException npgsqlException && npgsqlException.IsTransient; + private static bool IsTransientDatabaseException(Exception exception) + { + if (exception is DbUpdateException dbUpdateException) + { + return dbUpdateException.InnerException is not null && + IsTransientDatabaseException(dbUpdateException.InnerException); + } + + if (exception is NpgsqlException npgsqlException && npgsqlException.IsTransient) + { + return true; + } + + if (exception is TimeoutException or IOException) + { + return true; + } + + if (exception is InvalidOperationException invalidOperationException && + invalidOperationException.Message.Contains("transient failure", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return exception.InnerException is not null && + IsTransientDatabaseException(exception.InnerException); + } private static string? Truncate(string? value, int maxLength) { diff --git a/src/LiveRecorder.WebApi/Program.cs b/src/LiveRecorder.WebApi/Program.cs index 17a5780..cdcc4f0 100644 --- a/src/LiveRecorder.WebApi/Program.cs +++ b/src/LiveRecorder.WebApi/Program.cs @@ -23,6 +23,7 @@ using LiveRecorder.Infrastructure.Services; using LiveRecorder.WebApi.Middleware; using Microsoft.EntityFrameworkCore; using Microsoft.OpenApi.Models; +using Npgsql; var builder = WebApplication.CreateBuilder(args); var resetRecordingData = args.Contains("--reset-recording-data", StringComparer.OrdinalIgnoreCase); @@ -114,9 +115,32 @@ builder.Services.AddHttpClient("bilibili", client => var defaultConnection = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("ConnectionStrings:DefaultConnection is required."); +var connectionStringBuilder = new NpgsqlConnectionStringBuilder(defaultConnection); +if (connectionStringBuilder.Timeout <= 0) +{ + connectionStringBuilder.Timeout = 15; +} + +if (connectionStringBuilder.CommandTimeout <= 0) +{ + connectionStringBuilder.CommandTimeout = 60; +} + +if (connectionStringBuilder.KeepAlive <= 0) +{ + connectionStringBuilder.KeepAlive = 30; +} + builder.Services.AddDbContext(options => - options.UseNpgsql(defaultConnection, npgsql => - npgsql.MigrationsAssembly(typeof(LiveRecorderDbContext).Assembly.FullName))); + options.UseNpgsql(connectionStringBuilder.ConnectionString, npgsql => + { + npgsql.MigrationsAssembly(typeof(LiveRecorderDbContext).Assembly.FullName); + npgsql.CommandTimeout(connectionStringBuilder.CommandTimeout); + npgsql.EnableRetryOnFailure( + maxRetryCount: 5, + maxRetryDelay: TimeSpan.FromSeconds(10), + errorCodesToAdd: null); + })); builder.Services.AddScoped(provider => provider.GetRequiredService()); builder.Services.AddScoped();