Skip to content

Commit

Permalink
Reworked library refresh implementation #4
Browse files Browse the repository at this point in the history
* Renamed queue mechanism classes to a more generic wording. In the
future, auto-organize folders could be watched in realtime same like
library folders. In this case the FileRefreshQueue could be re-used to
wait until files are being locked no longer
* Made queue delay configurable
* Added xml comments
* Improved logging
*
  • Loading branch information
softworkz committed Dec 13, 2015
1 parent 9477371 commit 4901dd8
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@

namespace MediaBrowser.Server.Implementations.IO
{
class LibraryUpdateEventArgs : EventArgs
class FileRefreshEventArgs : EventArgs
{
private LibraryUpdateItem _item;
private FileRefreshItem _item;

public bool Cancel { get; set; }

public LibraryUpdateItem Item { get { return _item; }}
public FileRefreshItem Item { get { return _item; }}

public LibraryUpdateEventArgs(LibraryUpdateItem item)
public FileRefreshEventArgs(FileRefreshItem item)
{
_item = item;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@

namespace MediaBrowser.Server.Implementations.IO
{
class LibraryUpdateItem
class FileRefreshItem
{
private ConcurrentDictionary<string, string> _filePaths = new ConcurrentDictionary<string, string>();
private string _folder;

public LibraryUpdateItem(string folder)
public FileRefreshItem(string folder)
{
_folder = folder;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,54 +13,60 @@

namespace MediaBrowser.Server.Implementations.IO
{
class LibraryUpdateQueue
class FileRefreshQueue
{
/// <summary>
/// Delay in seconds for queue item processing.
/// </summary>
private int _queueRetryDelay;

/// <summary>
/// The file system abstraction object.
/// </summary>
private readonly IFileSystem _fileSystem;

/// <summary>
/// The timer lock
/// The timer lock.
/// </summary>
/// <remarks>Lock to prevent concurrent timer operations.</remarks>
private readonly object _timerLock = new object();

/// <summary>
/// The update timer
/// The queue timer.
/// </summary>
private Timer _updateTimer;
private Timer _queueTimer;

/// <summary>
/// The internal queue.
/// </summary>
private ConcurrentDictionary<string, LibraryUpdateItem> _internalQueue = new ConcurrentDictionary<string, LibraryUpdateItem>();
private ConcurrentDictionary<string, FileRefreshItem> _internalQueue = new ConcurrentDictionary<string, FileRefreshItem>();

/// <summary>
/// Occurs when an item is ready for processing.
/// </summary>
public event EventHandler<LibraryUpdateEventArgs> ItemReady;
public event EventHandler<FileRefreshEventArgs> ItemReady;

/// <summary>
/// Called when an item is due for processing.
/// Fired when an item is due for processing.
/// </summary>
/// <param name="item">The item.</param>
private bool OnItemReady(LibraryUpdateItem item)
private bool OnItemReady(FileRefreshItem item)
{
var e = new LibraryUpdateEventArgs(item);
var e = new FileRefreshEventArgs(item);

EventHelper.FireEventIfNotNull(ItemReady, this, e, Logger);

return !e.Cancel;
}

/// <summary>
/// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
/// Initializes a new instance of the <see cref="FileRefreshQueue" /> class.
/// </summary>
public LibraryUpdateQueue(ILogManager logManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem)
public FileRefreshQueue(ILogManager logManager, IFileSystem fileSystem, int queueRetryDelay)
{
Logger = logManager.GetLogger(GetType().Name);
ConfigurationManager = configurationManager;
_fileSystem = fileSystem;
_queueRetryDelay = queueRetryDelay;
}

/// <summary>
Expand All @@ -69,8 +75,10 @@ public LibraryUpdateQueue(ILogManager logManager, IServerConfigurationManager co
/// <value>The logger.</value>
private ILogger Logger { get; set; }

private IServerConfigurationManager ConfigurationManager { get; set; }

/// <summary>
/// Adds a path to the refresh queue.
/// </summary>
/// <param name="path">A path to a file or directory.</param>
public void AddPath(string path)
{
string folder;
Expand All @@ -89,7 +97,7 @@ public void AddPath(string path)
filePath = string.Empty;
}

var item = _internalQueue.GetOrAdd(folder, new LibraryUpdateItem(folder));
var item = _internalQueue.GetOrAdd(folder, new FileRefreshItem(folder));

if (!string.IsNullOrEmpty(filePath))
{
Expand All @@ -98,28 +106,37 @@ public void AddPath(string path)
item.FilePaths.TryAdd(path, path2);
}

item.DueDate = DateTime.Now.AddSeconds(ConfigurationManager.Configuration.RealtimeLibraryMonitorDelay);
item.DueDate = DateTime.Now.AddSeconds(_queueRetryDelay);

UpdateTimer();
}

public void AddItem(LibraryUpdateItem newItem)
/// <summary>
/// Re-inserts an item into the queue that could not be processed successfully.
/// </summary>
/// <param name="newItem"></param>
private void ReScheduleItem(FileRefreshItem newItem)
{
var item = _internalQueue.GetOrAdd(newItem.Folder, newItem);

if (item != newItem)
{
// Merge files from newItem with existing item's files
foreach (var path in newItem.FilePaths.Keys)
{
item.FilePaths.TryAdd(path, path.ToString());
}
}

item.DueDate = DateTime.Now.AddSeconds(ConfigurationManager.Configuration.RealtimeLibraryMonitorDelay);
// Postpone processing, no matter if new or updated
item.DueDate = DateTime.Now.AddSeconds(_queueRetryDelay);

UpdateTimer();
}

/// <summary>
/// Resets the timer to the time of the earliest due date of all items in the queue.
/// </summary>
private void UpdateTimer()
{
if (_internalQueue.Count == 0)
Expand All @@ -139,23 +156,30 @@ private void UpdateTimer()
SetTimer(nextTimerSpan);
}

/// <summary>
/// Clears the queue and disables the timer.
/// </summary>
public void Clear()
{
DisposeTimer();
_internalQueue.Clear();
}

/// <summary>
/// Sets the timer to fire once as soon as the interval specified by <paramref name="span"/> has elapsed.
/// </summary>
/// <param name="span"></param>
private void SetTimer(TimeSpan span)
{
lock (_timerLock)
{
if (_updateTimer == null)
if (_queueTimer == null)
{
_updateTimer = new Timer(TimerTick, null, span, TimeSpan.FromMilliseconds(-1));
_queueTimer = new Timer(TimerTick, null, span, TimeSpan.FromMilliseconds(-1));
}
else
{
_updateTimer.Change(span, TimeSpan.FromMilliseconds(-1));
_queueTimer.Change(span, TimeSpan.FromMilliseconds(-1));
}
}
}
Expand All @@ -164,10 +188,10 @@ private void DisposeTimer()
{
lock (_timerLock)
{
if (_updateTimer != null)
if (_queueTimer != null)
{
_updateTimer.Dispose();
_updateTimer = null;
_queueTimer.Dispose();
_queueTimer = null;
}
}
}
Expand All @@ -176,13 +200,13 @@ private void TimerTick(object stateInfo)
{
var itemFirst = _internalQueue.Where(i => i.Value.DueDate < DateTime.Now).OrderBy(e => e.Value.DueDate).FirstOrDefault();

LibraryUpdateItem item;
FileRefreshItem item;

if (_internalQueue.TryRemove(itemFirst.Key, out item))
{
if (!this.OnItemReady(item))
{
this.AddItem(item);
this.ReScheduleItem(item);
}
}

Expand Down
19 changes: 12 additions & 7 deletions MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public async void ReportFileSystemChangeComplete(string path, bool refreshPath)

private readonly IFileSystem _fileSystem;
private readonly IServerApplicationHost _appHost;
private LibraryUpdateQueue _updateQueue;
private FileRefreshQueue _updateQueue;

/// <summary>
/// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
Expand All @@ -121,13 +121,16 @@ public LibraryMonitor(ILogManager logManager, ITaskManager taskManager, ILibrary
throw new ArgumentNullException("taskManager");
}

// The default 40s of Configuration.RealtimeLibraryMonitorDelay seems to be a bit too long for the new implementation
int retryDelay = 15; // configurationManager.Configuration.RealtimeLibraryMonitorDelay

LibraryManager = libraryManager;
TaskManager = taskManager;
Logger = logManager.GetLogger(GetType().Name);
ConfigurationManager = configurationManager;
_fileSystem = fileSystem;
_appHost = appHost;
_updateQueue = new LibraryUpdateQueue(logManager, configurationManager, fileSystem);
_updateQueue = new FileRefreshQueue(logManager, fileSystem, retryDelay);
_updateQueue.ItemReady += UpdateQueue_ItemReady;
SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
}
Expand Down Expand Up @@ -418,14 +421,15 @@ public void ReportFileSystemChanged(string path)
_updateQueue.AddPath(path);
}

void UpdateQueue_ItemReady(object sender, LibraryUpdateEventArgs e)
void UpdateQueue_ItemReady(object sender, FileRefreshEventArgs e)
{
// Opposed to using foreach enumeration, .ToArray() returns a snapshot
// of the current dictionary contents
var affectedPathsSnapshot = e.Item.FilePaths.Keys.ToList();

if (affectedPathsSnapshot.Any(i => IsFileLocked(i)))
{
// Cancel will reschedule at a later time
e.Cancel = true;
return;
}
Expand Down Expand Up @@ -466,6 +470,7 @@ void UpdateQueue_ItemReady(object sender, LibraryUpdateEventArgs e)
return false;
}))
{
// Cancel will reschedule at a later time
e.Cancel = true;
return;
}
Expand Down Expand Up @@ -562,7 +567,7 @@ private async Task ProcessPathChanges(List<string> paths)

foreach (var p in paths)
{
Logger.Info(p + " reports change.");
Logger.Info("ProcessPathChanges: " + p + " reports change.");
}

// If the root folder changed, run the library task so the user can see it
Expand All @@ -575,7 +580,7 @@ private async Task ProcessPathChanges(List<string> paths)

foreach (var item in itemsToRefresh)
{
Logger.Info(item.Name + " (" + item.Path + ") will be refreshed.");
Logger.Info("ProcessPathChanges: " + item.Name + " (" + item.Path + ") will be refreshed.");

try
{
Expand All @@ -586,11 +591,11 @@ private async Task ProcessPathChanges(List<string> paths)
// For now swallow and log.
// Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
// Should we remove it from it's parent?
Logger.ErrorException("Error refreshing {0}", ex, item.Name);
Logger.ErrorException("ProcessPathChanges: Error refreshing {0}", ex, item.Name);
}
catch (Exception ex)
{
Logger.ErrorException("Error refreshing {0}", ex, item.Name);
Logger.ErrorException("ProcessPathChanges: Error refreshing {0}", ex, item.Name);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,9 @@
<Compile Include="HttpServer\SocketSharp\WebSocketSharpResponse.cs" />
<Compile Include="Intros\DefaultIntroProvider.cs" />
<Compile Include="IO\LibraryMonitor.cs" />
<Compile Include="IO\LibraryUpdateEventArgs.cs" />
<Compile Include="IO\LibraryUpdateItem.cs" />
<Compile Include="IO\LibraryUpdateQueue.cs" />
<Compile Include="IO\FileRefreshEventArgs.cs" />
<Compile Include="IO\FileRefreshItem.cs" />
<Compile Include="IO\FileRefreshQueue.cs" />
<Compile Include="Library\CoreResolutionIgnoreRule.cs" />
<Compile Include="Library\LibraryManager.cs" />
<Compile Include="Library\LocalTrailerPostScanTask.cs" />
Expand Down

0 comments on commit 4901dd8

Please sign in to comment.