Files
pgsql-jellyfin/Emby.Server.Implementations/IO/FileRefresher.cs
wjones 7eb2b445cb Postgres perf, path migration, and shutdown race fixes
- Major query performance boost: replaced correlated subqueries with DistinctBy() in BaseItemRepository, added episode deduplication index, and increased default command timeout to 120s.
- Fixed LibraryMonitor shutdown race: added disposal checks and exception handling in FileRefresher to prevent ObjectDisposedException.
- Added PowerShell and SQL utilities for fixing Linux paths in config files and database; new scripts for WebDir and path migration.
- Auto-fix for WebDir in startup.json on load; new helper scripts and docs.
- Added automated weekly DB performance monitoring scripts and reporting.
- Expanded documentation and README for all fixes, scripts, and migration steps.
- Updated SQL scripts for index creation and diagnostics; improved output handling.
- Updated db-config defaults and added new diagnostic/report files.
2026-03-05 08:56:42 -05:00

249 lines
6.7 KiB
C#

// <copyright file="FileRefresher.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO
{
public sealed class FileRefresher : IDisposable
{
private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
private readonly List<string> _affectedPaths = new();
private readonly Lock _timerLock = new();
private Timer? _timer;
private bool _disposed;
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
{
logger.LogDebug("New file refresher created for {0}", path);
Path = path;
_configurationManager = configurationManager;
_libraryManager = libraryManager;
_logger = logger;
AddPath(path);
}
public event EventHandler<EventArgs>? Completed;
public string Path { get; private set; }
private void AddAffectedPath(string path)
{
ArgumentException.ThrowIfNullOrEmpty(path);
if (!_affectedPaths.Contains(path, StringComparer.Ordinal))
{
_affectedPaths.Add(path);
}
}
public void AddPath(string path)
{
ArgumentException.ThrowIfNullOrEmpty(path);
lock (_timerLock)
{
AddAffectedPath(path);
}
RestartTimer();
}
public void RestartTimer()
{
if (_disposed)
{
return;
}
lock (_timerLock)
{
if (_disposed)
{
return;
}
if (_timer is null)
{
_timer = new Timer(OnTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
}
else
{
_timer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
}
}
}
public void ResetPath(string path, string? affectedFile)
{
lock (_timerLock)
{
_logger.LogDebug("Resetting file refresher from {0} to {1}", Path, path);
Path = path;
AddAffectedPath(path);
if (!string.IsNullOrEmpty(affectedFile))
{
AddAffectedPath(affectedFile);
}
}
RestartTimer();
}
private void OnTimerCallback(object? state)
{
// Check if disposed before processing
if (_disposed)
{
return;
}
List<string> paths;
lock (_timerLock)
{
paths = _affectedPaths.ToList();
}
_logger.LogDebug("Timer stopped.");
DisposeTimer();
Completed?.Invoke(this, EventArgs.Empty);
// Double-check disposal after timer is disposed
if (_disposed)
{
return;
}
try
{
ProcessPathChanges(paths);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing directory changes");
}
}
private void ProcessPathChanges(List<string> paths)
{
// Check if disposed before accessing services
if (_disposed)
{
return;
}
IEnumerable<BaseItem> itemsToRefresh;
try
{
itemsToRefresh = paths
.Distinct()
.Select(GetAffectedBaseItem)
.Where(item => item is not null)
.DistinctBy(x => x!.Id)!; // Removed null values in the previous .Where()
}
catch (ObjectDisposedException)
{
// Service provider has been disposed during shutdown, skip processing
_logger.LogDebug("Service provider disposed during path change processing, skipping refresh");
return;
}
foreach (var item in itemsToRefresh)
{
if (item is AggregateFolder)
{
continue;
}
_logger.LogInformation("{Name} ({Path}) will be refreshed.", item.Name, item.Path);
try
{
item.ChangedExternally();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {Name}", item.Name);
}
}
}
/// <summary>
/// Gets the affected base item.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>BaseItem.</returns>
private BaseItem? GetAffectedBaseItem(string path)
{
BaseItem? item = null;
while (item is null && !string.IsNullOrEmpty(path))
{
item = _libraryManager.FindByPath(path, null);
path = System.IO.Path.GetDirectoryName(path) ?? string.Empty;
}
if (item is not null)
{
// If the item has been deleted find the first valid parent that still exists
while (!Directory.Exists(item.Path) && !File.Exists(item.Path))
{
item = item.GetOwner() ?? item.GetParent();
if (item is null)
{
break;
}
}
}
return item;
}
private void DisposeTimer()
{
lock (_timerLock)
{
if (_timer is not null)
{
_timer.Dispose();
_timer = null;
}
}
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
DisposeTimer();
_disposed = true;
}
}
}