using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Database.Implementations; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Implementations.ScheduledTasks { /// /// Class PostgresPeriodicTuner. /// public class PostgresPeriodicTuner : IScheduledTask, IConfigurableScheduledTask { private readonly ILogger _logger; private readonly IDbContextFactory _dbContextFactory; /// /// Initializes a new instance of the class. /// /// The logger. /// The database context factory. public PostgresPeriodicTuner(ILogger logger, IDbContextFactory dbContextFactory) { _logger = logger; _dbContextFactory = dbContextFactory; } /// 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 progress. /// The cancellation token. /// A representing the asynchronous operation. public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { _logger.LogInformation("PostgreSQL Periodic Tuner task started."); try { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); 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.Database.SqlQueryRaw<(string TableName, long DeadTuples)>(DeadTupleQuery).ToListAsync(cancellationToken).ConfigureAwait(false)).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.Database.ExecuteSqlInterpolatedAsync($"VACUUM ANALYZE \"{table.TableName}\"", cancellationToken).ConfigureAwait(false); _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 = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks } }; } } }