Browse Source

Backport pull request #15672 from jellyfin/release-10.11.z

Cache OpenApi document generation

Original-merge: 8cd56521570992d8587db5e1f80d4cb826537f31

Merged-by: anthonylavado <anthony@lavado.ca>

Backported-by: Bond_009 <bond.009@outlook.com>
crobibero 2 days ago
parent
commit
6e74be0d46

+ 4 - 1
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -33,9 +33,11 @@ using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Cors.Infrastructure;
 using Microsoft.AspNetCore.Cors.Infrastructure;
 using Microsoft.AspNetCore.HttpOverrides;
 using Microsoft.AspNetCore.HttpOverrides;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
 using Microsoft.OpenApi.Any;
 using Microsoft.OpenApi.Any;
 using Microsoft.OpenApi.Interfaces;
 using Microsoft.OpenApi.Interfaces;
 using Microsoft.OpenApi.Models;
 using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.Swagger;
 using Swashbuckle.AspNetCore.SwaggerGen;
 using Swashbuckle.AspNetCore.SwaggerGen;
 using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
 using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
 
 
@@ -259,7 +261,8 @@ namespace Jellyfin.Server.Extensions
                 c.OperationFilter<FileRequestFilter>();
                 c.OperationFilter<FileRequestFilter>();
                 c.OperationFilter<ParameterObsoleteFilter>();
                 c.OperationFilter<ParameterObsoleteFilter>();
                 c.DocumentFilter<AdditionalModelFilter>();
                 c.DocumentFilter<AdditionalModelFilter>();
-            });
+            })
+            .Replace(ServiceDescriptor.Transient<ISwaggerProvider, CachingOpenApiProvider>());
         }
         }
 
 
         private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationRequirement authorizationRequirement)
         private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationRequirement authorizationRequirement)

+ 89 - 0
Jellyfin.Server/Filters/CachingOpenApiProvider.cs

@@ -0,0 +1,89 @@
+using System;
+using System.Threading;
+using Microsoft.AspNetCore.Mvc.ApiExplorer;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.Swagger;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace Jellyfin.Server.Filters;
+
+/// <summary>
+/// OpenApi provider with caching.
+/// </summary>
+internal sealed class CachingOpenApiProvider : ISwaggerProvider
+{
+    private const string CacheKey = "openapi.json";
+
+    private static readonly MemoryCacheEntryOptions _cacheOptions = new() { SlidingExpiration = TimeSpan.FromMinutes(5) };
+    private static readonly SemaphoreSlim _lock = new(1, 1);
+    private static readonly TimeSpan _lockTimeout = TimeSpan.FromSeconds(1);
+
+    private readonly IMemoryCache _memoryCache;
+    private readonly SwaggerGenerator _swaggerGenerator;
+    private readonly SwaggerGeneratorOptions _swaggerGeneratorOptions;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="CachingOpenApiProvider"/> class.
+    /// </summary>
+    /// <param name="optionsAccessor">The options accessor.</param>
+    /// <param name="apiDescriptionsProvider">The api descriptions provider.</param>
+    /// <param name="schemaGenerator">The schema generator.</param>
+    /// <param name="memoryCache">The memory cache.</param>
+    public CachingOpenApiProvider(
+        IOptions<SwaggerGeneratorOptions> optionsAccessor,
+        IApiDescriptionGroupCollectionProvider apiDescriptionsProvider,
+        ISchemaGenerator schemaGenerator,
+        IMemoryCache memoryCache)
+    {
+        _swaggerGeneratorOptions = optionsAccessor.Value;
+        _swaggerGenerator = new SwaggerGenerator(_swaggerGeneratorOptions, apiDescriptionsProvider, schemaGenerator);
+        _memoryCache = memoryCache;
+    }
+
+    /// <inheritdoc />
+    public OpenApiDocument GetSwagger(string documentName, string? host = null, string? basePath = null)
+    {
+        if (_memoryCache.TryGetValue(CacheKey, out OpenApiDocument? openApiDocument) && openApiDocument is not null)
+        {
+            return AdjustDocument(openApiDocument, host, basePath);
+        }
+
+        var acquired = _lock.Wait(_lockTimeout);
+        try
+        {
+            if (_memoryCache.TryGetValue(CacheKey, out openApiDocument) && openApiDocument is not null)
+            {
+                return AdjustDocument(openApiDocument, host, basePath);
+            }
+
+            if (!acquired)
+            {
+                throw new InvalidOperationException("OpenApi document is generating");
+            }
+
+            openApiDocument = _swaggerGenerator.GetSwagger(documentName);
+            _memoryCache.Set(CacheKey, openApiDocument, _cacheOptions);
+            return AdjustDocument(openApiDocument, host, basePath);
+        }
+        finally
+        {
+            if (acquired)
+            {
+                _lock.Release();
+            }
+        }
+    }
+
+    private OpenApiDocument AdjustDocument(OpenApiDocument document, string? host, string? basePath)
+    {
+        document.Servers = _swaggerGeneratorOptions.Servers.Count != 0
+            ? _swaggerGeneratorOptions.Servers
+            : string.IsNullOrEmpty(host) && string.IsNullOrEmpty(basePath)
+                ? []
+                : [new OpenApiServer { Url = $"{host}{basePath}" }];
+
+        return document;
+    }
+}