From 6e74be0d46f409b7b63f02a29cbbbd572f40bd32 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 3 Dec 2025 14:04:28 -0500 Subject: Backport pull request #15672 from jellyfin/release-10.11.z Cache OpenApi document generation Original-merge: 8cd56521570992d8587db5e1f80d4cb826537f31 Merged-by: anthonylavado Backported-by: Bond_009 --- Jellyfin.Server/Filters/CachingOpenApiProvider.cs | 89 +++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 Jellyfin.Server/Filters/CachingOpenApiProvider.cs (limited to 'Jellyfin.Server/Filters') diff --git a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs new file mode 100644 index 000000000..4169f2fb3 --- /dev/null +++ b/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; + +/// +/// OpenApi provider with caching. +/// +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; + + /// + /// Initializes a new instance of the class. + /// + /// The options accessor. + /// The api descriptions provider. + /// The schema generator. + /// The memory cache. + public CachingOpenApiProvider( + IOptions optionsAccessor, + IApiDescriptionGroupCollectionProvider apiDescriptionsProvider, + ISchemaGenerator schemaGenerator, + IMemoryCache memoryCache) + { + _swaggerGeneratorOptions = optionsAccessor.Value; + _swaggerGenerator = new SwaggerGenerator(_swaggerGeneratorOptions, apiDescriptionsProvider, schemaGenerator); + _memoryCache = memoryCache; + } + + /// + 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; + } +} -- cgit v1.2.3