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 } }; } } }