From a0c38131c8822003eb28f220d40791283e271f53 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:31:04 -0500 Subject: [PATCH] Wire NpgsqlDataSource pool into DI for PostgreSQL provider (#10) * Initial plan * Add NpgsqlDataSource pool wiring to DI (Issue 1.4)" - PostgreSqlDatabaseProvider: accept NpgsqlDataSource via constructor injection, use it in Initialise() - PostgreSqlDesignTimeJellyfinDbFactory: build NpgsqlDataSource from connection string for design-time use - ServiceCollectionExtensions: register NpgsqlDataSource as singleton with pool params (MinPoolSize=2, MaxPoolSize=20, CommandTimeout=30) from CustomProviderOptions.Options Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ZoltyMat <177592743+ZoltyMat@users.noreply.github.com> --- .../Extensions/ServiceCollectionExtensions.cs | 34 +++++++++++++++++++ .../PostgreSqlDatabaseProvider.cs | 25 +++++++------- .../PostgreSqlDesignTimeJellyfinDbFactory.cs | 14 ++++++-- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index 75ac3f921..aed695c35 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -13,6 +13,7 @@ using MediaBrowser.Controller.Configuration; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Npgsql; using JellyfinDbProviderFactory = System.Func; namespace Jellyfin.Server.Implementations.Extensions; @@ -28,6 +29,12 @@ public static class ServiceCollectionExtensions yield return typeof(PostgreSqlDatabaseProvider); } + private static int GetPoolOption(IEnumerable? options, string key, int defaultValue) + { + var value = options?.FirstOrDefault(o => o.Key.Equals(key, StringComparison.OrdinalIgnoreCase))?.Value; + return int.TryParse(value, out var parsed) ? parsed : defaultValue; + } + private static IDictionary GetSupportedDbProviders() { var items = new Dictionary(StringComparer.InvariantCultureIgnoreCase); @@ -125,6 +132,33 @@ public static class ServiceCollectionExtensions serviceCollection.AddSingleton(providerFactory!); + if (efCoreConfiguration.DatabaseType.Equals("Jellyfin-PostgreSQL", StringComparison.OrdinalIgnoreCase)) + { + serviceCollection.AddSingleton(static sp => + { + var config = sp.GetRequiredService().GetConfiguration("database"); + var options = config.CustomProviderOptions?.Options; + + var connectionString = + Environment.GetEnvironmentVariable("POSTGRES_CONNECTION_STRING") + ?? options + ?.FirstOrDefault(o => o.Key.Equals("ConnectionString", StringComparison.OrdinalIgnoreCase)) + ?.Value + ?? config.CustomProviderOptions?.ConnectionString + ?? throw new InvalidOperationException( + "No PostgreSQL connection string found. Set the POSTGRES_CONNECTION_STRING environment variable, " + + "or provide it via CustomProviderOptions.Options[\"ConnectionString\"] or CustomProviderOptions.ConnectionString."); + + var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); + + dataSourceBuilder.ConnectionStringBuilder.MinPoolSize = GetPoolOption(options, "MinPoolSize", 2); + dataSourceBuilder.ConnectionStringBuilder.MaxPoolSize = GetPoolOption(options, "MaxPoolSize", 20); + dataSourceBuilder.ConnectionStringBuilder.CommandTimeout = GetPoolOption(options, "CommandTimeout", 30); + + return dataSourceBuilder.Build(); + }); + } + switch (efCoreConfiguration.LockingBehavior) { case DatabaseLockingBehaviorTypes.NoLock: diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/PostgreSqlDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/PostgreSqlDatabaseProvider.cs index 1c919d228..9b0367685 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/PostgreSqlDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/PostgreSqlDatabaseProvider.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.DbConfiguration; using Microsoft.EntityFrameworkCore; +using Npgsql; namespace Jellyfin.Database.Providers.PostgreSQL; @@ -18,24 +18,25 @@ public sealed class PostgreSqlDatabaseProvider : IJellyfinDatabaseProvider private const string BackupNotSupportedMessage = "Automated migration backups are not supported for PostgreSQL. Use the jellyfin-pg-backup CronJob for nightly S3 backups."; + private readonly NpgsqlDataSource _dataSource; + + /// + /// Initializes a new instance of the class. + /// + /// The used for PostgreSQL connections. + public PostgreSqlDatabaseProvider(NpgsqlDataSource dataSource) + { + _dataSource = dataSource; + } + /// public IDbContextFactory? DbContextFactory { get; set; } /// public void Initialise(DbContextOptionsBuilder options, DatabaseConfigurationOptions databaseConfiguration) { - var connectionString = - Environment.GetEnvironmentVariable("POSTGRES_CONNECTION_STRING") - ?? databaseConfiguration.CustomProviderOptions?.Options - ?.FirstOrDefault(o => o.Key.Equals("ConnectionString", StringComparison.OrdinalIgnoreCase)) - ?.Value - ?? databaseConfiguration.CustomProviderOptions?.ConnectionString - ?? throw new InvalidOperationException( - "No PostgreSQL connection string found. Set the POSTGRES_CONNECTION_STRING environment variable, " + - "or provide it via CustomProviderOptions.Options[\"ConnectionString\"] or CustomProviderOptions.ConnectionString."); - options.UseNpgsql( - connectionString, + _dataSource, o => o.MigrationsAssembly(GetType().Assembly.FullName)); } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/PostgreSqlDesignTimeJellyfinDbFactory.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/PostgreSqlDesignTimeJellyfinDbFactory.cs index 342d0693b..2c1421383 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/PostgreSqlDesignTimeJellyfinDbFactory.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/PostgreSqlDesignTimeJellyfinDbFactory.cs @@ -4,6 +4,7 @@ using Jellyfin.Database.Implementations.Locking; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Logging.Abstractions; +using Npgsql; namespace Jellyfin.Database.Providers.PostgreSQL; @@ -21,12 +22,21 @@ internal sealed class PostgreSqlDesignTimeJellyfinDbFactory : IDesignTimeDbConte ?? "Host=localhost;Database=jellyfin;Username=postgres;Password=postgres"; var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(connectionString, o => o.MigrationsAssembly(GetType().Assembly)); + + // Build a NpgsqlDataSource for EF Core configuration. The DI-owned singleton data source + // is not available in design-time context; this instance is intentionally not disposed here + // because EF Core holds a reference to it for the lifetime of the returned context. + // As a design-time-only factory (used only for dotnet-ef CLI operations), the process + // exits after the migration is applied, which releases all resources. +#pragma warning disable CA2000 // Dispose objects before losing scope + var dataSource = new NpgsqlDataSourceBuilder(connectionString).Build(); +#pragma warning restore CA2000 // Dispose objects before losing scope + optionsBuilder.UseNpgsql(dataSource, o => o.MigrationsAssembly(GetType().Assembly)); return new JellyfinDbContext( optionsBuilder.Options, NullLogger.Instance, - new PostgreSqlDatabaseProvider(), + new PostgreSqlDatabaseProvider(dataSource), new NoLockBehavior(NullLogger.Instance)); } }