Updated contributors, upgraded to AsyncKeyedLocker 6.3.0 which now supports non-keyed locking using a similar interface and changed SemaphoreSlim-based locks to using AsyncNonKeyedLocker.

This commit is contained in:
Mark Cilia Vincenti 2024-01-14 12:11:16 +01:00
parent 6a257e1b40
commit e47144e7c7
14 changed files with 126 additions and 149 deletions

View File

@ -77,6 +77,7 @@
- [Marenz](https://github.com/Marenz)
- [marius-luca-87](https://github.com/marius-luca-87)
- [mark-monteiro](https://github.com/mark-monteiro)
- [MarkCiliaVincenti](https://github.com/MarkCiliaVincenti)
- [Matt07211](https://github.com/Matt07211)
- [Maxr1998](https://github.com/Maxr1998)
- [mcarlton00](https://github.com/mcarlton00)

View File

@ -4,7 +4,7 @@
</PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies">
<PackageVersion Include="AsyncKeyedLock" Version="6.2.6" />
<PackageVersion Include="AsyncKeyedLock" Version="6.3.0" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />

View File

@ -11,6 +11,7 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
@ -51,7 +52,7 @@ namespace Emby.Server.Implementations.Library
private readonly IDirectoryService _directoryService;
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private IMediaSourceProvider[] _providers;
@ -467,12 +468,10 @@ namespace Emby.Server.Implementations.Library
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
{
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
MediaSourceInfo mediaSource;
ILiveStream liveStream;
try
using (await _liveStreamLocker.LockAsync(cancellationToken).ConfigureAwait(false))
{
var (provider, keyId) = GetProvider(request.OpenToken);
@ -492,10 +491,6 @@ namespace Emby.Server.Implementations.Library
_openStreams[mediaSource.LiveStreamId] = liveStream;
}
finally
{
_liveStreamSemaphore.Release();
}
try
{
@ -836,9 +831,7 @@ namespace Emby.Server.Implementations.Library
{
ArgumentException.ThrowIfNullOrEmpty(id);
await _liveStreamSemaphore.WaitAsync().ConfigureAwait(false);
try
using (await _liveStreamLocker.LockAsync().ConfigureAwait(false))
{
if (_openStreams.TryGetValue(id, out ILiveStream liveStream))
{
@ -857,10 +850,6 @@ namespace Emby.Server.Implementations.Library
}
}
}
finally
{
_liveStreamSemaphore.Release();
}
}
private (IMediaSourceProvider MediaSourceProvider, string KeyId) GetProvider(string key)
@ -897,7 +886,7 @@ namespace Emby.Server.Implementations.Library
CloseLiveStream(key).GetAwaiter().GetResult();
}
_liveStreamSemaphore.Dispose();
_liveStreamLocker.Dispose();
}
}
}

View File

@ -26,6 +26,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AsyncKeyedLock" />
<PackageReference Include="EFCoreSecondLevelCacheInterceptor" />
<PackageReference Include="System.Linq.Async" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
@ -37,7 +38,7 @@ public class TrickplayManager : ITrickplayManager
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IApplicationPaths _appPaths;
private static readonly SemaphoreSlim _resourcePool = new(1, 1);
private static readonly AsyncNonKeyedLocker _resourcePool = new(1);
private static readonly string[] _trickplayImgExtensions = { ".jpg" };
/// <summary>
@ -107,93 +108,92 @@ public class TrickplayManager : ITrickplayManager
var imgTempDir = string.Empty;
var outputDir = GetTrickplayDirectory(video, width);
await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
try
using (await _resourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
{
if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width))
{
_logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
return;
}
// Extract images
// Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
if (mediaSource is null)
{
_logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
return;
}
var mediaPath = mediaSource.Path;
var mediaStream = mediaSource.VideoStream;
var container = mediaSource.Container;
_logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
mediaPath,
container,
mediaSource,
mediaStream,
width,
TimeSpan.FromMilliseconds(options.Interval),
options.EnableHwAcceleration,
options.ProcessThreads,
options.Qscale,
options.ProcessPriority,
_encodingHelper,
cancellationToken).ConfigureAwait(false);
if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir))
{
throw new InvalidOperationException("Null or invalid directory from media encoder.");
}
var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false)
.Select(i => i.FullName)
.OrderBy(i => i)
.ToList();
// Create tiles
var trickplayInfo = CreateTiles(images, width, options, outputDir);
// Save tiles info
try
{
if (trickplayInfo is not null)
if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width))
{
trickplayInfo.ItemId = video.Id;
await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false);
_logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
_logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
return;
}
else
// Extract images
// Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
if (mediaSource is null)
{
throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
_logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
return;
}
var mediaPath = mediaSource.Path;
var mediaStream = mediaSource.VideoStream;
var container = mediaSource.Container;
_logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
mediaPath,
container,
mediaSource,
mediaStream,
width,
TimeSpan.FromMilliseconds(options.Interval),
options.EnableHwAcceleration,
options.ProcessThreads,
options.Qscale,
options.ProcessPriority,
_encodingHelper,
cancellationToken).ConfigureAwait(false);
if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir))
{
throw new InvalidOperationException("Null or invalid directory from media encoder.");
}
var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false)
.Select(i => i.FullName)
.OrderBy(i => i)
.ToList();
// Create tiles
var trickplayInfo = CreateTiles(images, width, options, outputDir);
// Save tiles info
try
{
if (trickplayInfo is not null)
{
trickplayInfo.ItemId = video.Id;
await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false);
_logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
}
else
{
throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while saving trickplay tiles info.");
// Make sure no files stay in metadata folders on failure
// if tiles info wasn't saved.
Directory.Delete(outputDir, true);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while saving trickplay tiles info.");
// Make sure no files stay in metadata folders on failure
// if tiles info wasn't saved.
Directory.Delete(outputDir, true);
_logger.LogError(ex, "Error creating trickplay images.");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating trickplay images.");
}
finally
{
_resourcePool.Release();
if (!string.IsNullOrEmpty(imgTempDir))
finally
{
Directory.Delete(imgTempDir, true);
if (!string.IsNullOrEmpty(imgTempDir))
{
Directory.Delete(imgTempDir, true);
}
}
}
}

View File

@ -100,6 +100,6 @@ public interface ITranscodeManager
/// </summary>
/// <param name="outputPath">The output path of the transcoded file.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="SemaphoreSlim"/>.</returns>
/// <returns>An <see cref="IDisposable"/>.</returns>
ValueTask<IDisposable> LockAsync(string outputPath, CancellationToken cancellationToken);
}

View File

@ -11,6 +11,7 @@ using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
@ -60,7 +61,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly IServerConfigurationManager _serverConfig;
private readonly string _startupOptionFFmpegPath;
private readonly SemaphoreSlim _thumbnailResourcePool;
private readonly AsyncNonKeyedLocker _thumbnailResourcePool;
private readonly object _runningProcessesLock = new object();
private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
@ -116,7 +117,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
var semaphoreCount = 2 * Environment.ProcessorCount;
_thumbnailResourcePool = new SemaphoreSlim(semaphoreCount, semaphoreCount);
_thumbnailResourcePool = new(semaphoreCount);
}
/// <inheritdoc />
@ -754,8 +755,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
bool ranToCompletion;
await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
try
using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
{
StartProcess(processWrapper);
@ -776,10 +776,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
ranToCompletion = false;
}
}
finally
{
_thumbnailResourcePool.Release();
}
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
var file = _fileSystem.GetFileInfo(tempExtractPath);

View File

@ -727,7 +727,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
/// </summary>
/// <param name="outputPath">The output path of the transcoded file.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="SemaphoreSlim"/>.</returns>
/// <returns>An <see cref="IDisposable"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ValueTask<IDisposable> LockAsync(string outputPath, CancellationToken cancellationToken)
{

View File

@ -7,6 +7,7 @@ using System.Net.Mime;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@ -38,7 +39,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
private readonly IServerApplicationPaths _appPaths;
private readonly IImageEncoder _imageEncoder;
private readonly SemaphoreSlim _parallelEncodingLimit;
private readonly AsyncNonKeyedLocker _parallelEncodingLimit;
private bool _disposed;
@ -68,7 +69,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
semaphoreCount = 2 * Environment.ProcessorCount;
}
_parallelEncodingLimit = new(semaphoreCount, semaphoreCount);
_parallelEncodingLimit = new(semaphoreCount);
}
private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images");
@ -193,18 +194,13 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
{
if (!File.Exists(cacheFilePath))
{
// Limit number of parallel (more precisely: concurrent) image encodings to prevent a high memory usage
await _parallelEncodingLimit.WaitAsync().ConfigureAwait(false);
string resultPath;
try
// Limit number of parallel (more precisely: concurrent) image encodings to prevent a high memory usage
using (await _parallelEncodingLimit.LockAsync().ConfigureAwait(false))
{
resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
}
finally
{
_parallelEncodingLimit.Release();
}
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
{

View File

@ -21,4 +21,8 @@
<Compile Include="..\..\SharedVersion.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AsyncKeyedLock" />
</ItemGroup>
</Project>

View File

@ -8,6 +8,7 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
@ -50,7 +51,7 @@ namespace Jellyfin.LiveTv.Channels
private readonly IFileSystem _fileSystem;
private readonly IProviderManager _providerManager;
private readonly IMemoryCache _memoryCache;
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
private readonly AsyncNonKeyedLocker _resourcePool = new(1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private bool _disposed = false;
@ -832,9 +833,7 @@ namespace Jellyfin.LiveTv.Channels
{
}
await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
try
using (await _resourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
{
try
{
@ -881,10 +880,6 @@ namespace Jellyfin.LiveTv.Channels
return result;
}
finally
{
_resourcePool.Release();
}
}
private async Task CacheResponse(ChannelItemResult result, string path)

View File

@ -14,6 +14,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using AsyncKeyedLock;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Extensions;
@ -68,7 +69,7 @@ namespace Jellyfin.LiveTv.EmbyTV
private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels =
new ConcurrentDictionary<string, EpgChannelData>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1);
private readonly AsyncNonKeyedLocker _recordingDeleteSemaphore = new(1);
private bool _disposed;
@ -1447,9 +1448,7 @@ namespace Jellyfin.LiveTv.EmbyTV
return;
}
await _recordingDeleteSemaphore.WaitAsync().ConfigureAwait(false);
try
using (await _recordingDeleteSemaphore.LockAsync().ConfigureAwait(false))
{
if (_disposed)
{
@ -1502,10 +1501,6 @@ namespace Jellyfin.LiveTv.EmbyTV
}
}
}
finally
{
_recordingDeleteSemaphore.Release();
}
}
private void DeleteLibraryItemsForTimers(List<TimerInfo> timers)

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@ -11,6 +11,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AsyncKeyedLock" />
<PackageReference Include="Jellyfin.XmlTv" />
<PackageReference Include="System.Linq.Async" />
</ItemGroup>

View File

@ -16,6 +16,7 @@ using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
@ -35,7 +36,7 @@ namespace Jellyfin.LiveTv.Listings
private readonly ILogger<SchedulesDirect> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
private readonly AsyncNonKeyedLocker _tokenLock = new(1);
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
@ -573,27 +574,25 @@ namespace Jellyfin.LiveTv.Listings
}
}
await _tokenSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
using (await _tokenLock.LockAsync(cancellationToken).ConfigureAwait(false))
{
var result = await GetTokenInternal(username, password, cancellationToken).ConfigureAwait(false);
savedToken.Name = result;
savedToken.Value = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture);
return result;
}
catch (HttpRequestException ex)
{
if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
try
{
_tokens.Clear();
_lastErrorResponse = DateTime.UtcNow;
var result = await GetTokenInternal(username, password, cancellationToken).ConfigureAwait(false);
savedToken.Name = result;
savedToken.Value = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture);
return result;
}
catch (HttpRequestException ex)
{
if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
{
_tokens.Clear();
_lastErrorResponse = DateTime.UtcNow;
}
throw;
}
finally
{
_tokenSemaphore.Release();
throw;
}
}
}
@ -801,7 +800,7 @@ namespace Jellyfin.LiveTv.Listings
if (disposing)
{
_tokenSemaphore?.Dispose();
_tokenLock?.Dispose();
}
_disposed = true;