Przeglądaj źródła

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.

Mark Cilia Vincenti 1 rok temu
rodzic
commit
e47144e7c7

+ 1 - 0
CONTRIBUTORS.md

@@ -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)

+ 1 - 1
Directory.Packages.props

@@ -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" />

+ 5 - 16
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -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();
             }
         }
     }

+ 1 - 0
Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj

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

+ 70 - 70
Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs

@@ -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)
+            try
             {
-                _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
-                return;
-            }
+                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;
+                }
 
-            var mediaPath = mediaSource.Path;
-            var mediaStream = mediaSource.VideoStream;
-            var container = mediaSource.Container;
+                // 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));
 
-            _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 (mediaSource is null)
+                {
+                    _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
+                    return;
+                }
 
-            if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir))
-            {
-                throw new InvalidOperationException("Null or invalid directory from media encoder.");
-            }
+                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();
+                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);
+                // Create tiles
+                var trickplayInfo = CreateTiles(images, width, options, outputDir);
 
-            // Save tiles info
-            try
-            {
-                if (trickplayInfo is not null)
+                // Save tiles info
+                try
                 {
-                    trickplayInfo.ItemId = video.Id;
-                    await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false);
+                    if (trickplayInfo is not null)
+                    {
+                        trickplayInfo.ItemId = video.Id;
+                        await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false);
 
-                    _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
+                        _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
+                    }
+                    else
+                    {
+                        throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
+                    }
                 }
-                else
+                catch (Exception ex)
                 {
-                    throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
+                    _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);
+                }
             }
         }
     }

+ 1 - 1
MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs

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

+ 4 - 8
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -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);

+ 1 - 1
MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs

@@ -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)
     {

+ 6 - 10
src/Jellyfin.Drawing/ImageProcessor.cs

@@ -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))
                 {

+ 4 - 0
src/Jellyfin.Drawing/Jellyfin.Drawing.csproj

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

+ 3 - 8
src/Jellyfin.LiveTv/Channels/ChannelManager.cs

@@ -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)

+ 3 - 8
src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs

@@ -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)

+ 2 - 1
src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj

@@ -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>

+ 18 - 19
src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs

@@ -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
-            {
-                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)
+            using (await _tokenLock.LockAsync(cancellationToken).ConfigureAwait(false))
             {
-                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;