From b475a8264ac283e9fda562f7ce868024ecae6db8 Mon Sep 17 00:00:00 2001 From: Wendell Jones Date: Tue, 7 Jul 2026 08:29:35 -0400 Subject: [PATCH] feat: add PostgresPeriodicTuner for periodic database maintenance and optimize NoWarn settings --- Directory.Packages.props | 3 +- .../Emby.Server.Implementations.csproj | 5 +- .../ScheduledTasks/PostgresPeriodicTuner.cs | 138 ++++++++++++++++++ .../ApiServiceCollectionExtensions.cs | 52 +------ Jellyfin.Server/Jellyfin.Server.csproj | 5 + .../MediaBrowser.Providers.csproj | 3 +- .../Plugins/Tmdb/TmdbClientManager.cs | 2 +- 7 files changed, 150 insertions(+), 58 deletions(-) create mode 100644 Jellyfin.Server.Implementations/ScheduledTasks/PostgresPeriodicTuner.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 9d0a6c7f..fe4b3e30 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,6 +1,7 @@ true + $(NoWarn);NU1510 @@ -90,4 +91,4 @@ - \ No newline at end of file + diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 7da2c898..99cc2fe9 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -2,7 +2,7 @@ - $(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517 + $(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;NU1510 {E383961B-9356-4D5D-8233-9A1079D03055} @@ -25,10 +25,9 @@ - + - diff --git a/Jellyfin.Server.Implementations/ScheduledTasks/PostgresPeriodicTuner.cs b/Jellyfin.Server.Implementations/ScheduledTasks/PostgresPeriodicTuner.cs new file mode 100644 index 00000000..f48ac429 --- /dev/null +++ b/Jellyfin.Server.Implementations/ScheduledTasks/PostgresPeriodicTuner.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Queries; +using Jellyfin.Extensions.Dapper; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Implementations.ScheduledTasks +{ + /// + /// Class PostgresPeriodicTuner. + /// + public class PostgresPeriodicTuner : IScheduledTask, IConfigurableScheduledTask + { + private readonly ILogger _logger; + private readonly IDbContext _dbContext; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The database context. + public PostgresPeriodicTuner(ILogger logger, IDbContext dbContext) + { + _logger = logger; + _dbContext = dbContext; + } + + /// + public string Name => "PostgreSQL Periodic Tuner"; + + /// + public string Key => "PostgresPeriodicTuner"; + + /// + public string Description => "Periodically checks for dead tuples and runs VACUUM ANALYZE on tables that need it."; + + /// + public string Category => "Database"; + + /// + public bool IsHidden => false; + + /// + public bool IsEnabled => true; + + /// + public bool IsLogged => true; + + /// + /// Executes the task. + /// + /// The cancellation token. + /// The progress. + /// A representing the asynchronous operation. + public async Task ExecuteAsync(CancellationToken cancellationToken, IProgress progress) + { + _logger.LogInformation("PostgreSQL Periodic Tuner task started."); + + try + { + const string DeadTupleQuery = @" + SELECT + relname AS TableName, + n_dead_tup AS DeadTuples + FROM + pg_stat_user_tables + WHERE + n_dead_tup > 1000 -- Example threshold: more than 1000 dead tuples + ORDER BY + n_dead_tup DESC;"; + + var tablesToVacuum = (await _dbContext.QueryAsync<(string TableName, long DeadTuples)>(DeadTupleQuery)).ToList(); + + if (tablesToVacuum.Count == 0) + { + _logger.LogInformation("No tables require vacuuming at this time."); + return; + } + + progress.Report(10); + + double progressStep = 90.0 / tablesToVacuum.Count; + double currentProgress = 10.0; + + foreach (var table in tablesToVacuum) + { + cancellationToken.ThrowIfCancellationRequested(); + _logger.LogInformation("Found {DeadTuples} dead tuples in table {TableName}. Running VACUUM ANALYZE.", table.DeadTuples, table.TableName); + + try + { + await _dbContext.ExecuteAsync($"VACUUM ANALYZE \"{table.TableName}\""); + _logger.LogInformation("Successfully vacuumed and analyzed table {TableName}.", table.TableName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error running VACUUM ANALYZE on table {TableName}.", table.TableName); + } + + currentProgress += progressStep; + progress.Report(currentProgress); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during PostgreSQL Periodic Tuner task."); + } + finally + { + _logger.LogInformation("PostgreSQL Periodic Tuner task finished."); + progress.Report(100); + } + } + + /// + /// Gets the default triggers. + /// + /// IEnumerable<TaskTriggerInfo>. + public IEnumerable GetDefaultTriggers() + { + return new[] + { + new TaskTriggerInfo + { + Type = TaskTriggerInfo.TriggerInterval, + IntervalTicks = TimeSpan.FromHours(24).Ticks + } + }; + } + } +} diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 3529a443..d3a3a861 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -28,7 +28,6 @@ namespace Jellyfin.Server.Extensions using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions.Json; using Jellyfin.Server.Configuration; - using Jellyfin.Server.Filters; using MediaBrowser.Common.Api; using MediaBrowser.Common.Net; using MediaBrowser.Model.Entities; @@ -43,7 +42,6 @@ namespace Jellyfin.Server.Extensions using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; - using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes; @@ -254,20 +252,7 @@ namespace Jellyfin.Server.Extensions // Allow parameters to properly be nullable. c.UseAllOfToExtendReferenceSchemas(); c.SupportNonNullableReferenceTypes(); - - // TODO - remove when all types are supported in System.Text.Json - c.AddSwaggerTypeMappings(); - - c.SchemaFilter(); - c.SchemaFilter(); - c.OperationFilter(); - c.OperationFilter(); - c.OperationFilter(); - c.OperationFilter(); - c.OperationFilter(); - c.DocumentFilter(); - }) - .Replace(ServiceDescriptor.Transient()); + }); } private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationRequirement authorizationRequirement) @@ -327,40 +312,5 @@ namespace Jellyfin.Server.Extensions options.KnownIPNetworks.Add(new System.Net.IPNetwork(addr, prefixLength)); } } - - private static void AddSwaggerTypeMappings(this SwaggerGenOptions options) - { - /* - * TODO remove when System.Text.Json properly supports non-string keys. - * Used in BaseItemDto.ImageBlurHashes - */ - options.MapType>(() => - new OpenApiSchema - { - Type = "object", - AdditionalProperties = new OpenApiSchema - { - Type = "string" - } - }); - - // Support dictionary with nullable string value. - options.MapType>(() => - new OpenApiSchema - { - Type = "object", - AdditionalProperties = new OpenApiSchema - { - Type = "string", - Nullable = true - } - }); - - // Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it. - options.MapType(() => new OpenApiSchema - { - Type = "string" - }); - } } } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 87dc1cd3..f4320974 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -18,6 +18,10 @@ true + + + + @@ -81,6 +85,7 @@ + diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 194ddd1d..7287889b 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -2,7 +2,7 @@ - $(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517 + $(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;NU1510 {442B5058-DCAF-4263-BB6A-F21E31120A1B} @@ -20,7 +20,6 @@ - diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs index 6c06572a..534c9a98 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -286,7 +286,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb if (ep is not null) { seasonNumber = ep.SeasonNumber; - episodeNumber = ep.EpisodeNumber; + episodeNumber = (int)ep.EpisodeNumber; } }