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