From 9cc895e0ed56fb98811cfaeafdc4cd0fb0a7dac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Henrique=20Inoc=C3=AAncio=20Borba=20Ferreira?= Date: Thu, 18 Jan 2024 13:18:55 -0800 Subject: [PATCH] [Cosmos DB] Query max concurrency flag (#3669) * Extending queries to use max concurrency parallelism * Add new Action Filter for Query Latency Over Efficiency * Adding new tests to ensure that FhirController has the expected attributes. Adding more tests on top of the new filter. Limiting the execution of the filter to Azure API for FHIR only. --- .../Features/KnownHeaders.cs | 1 + .../AzureApiForFhirRuntimeConfiguration.cs | 2 + ...eHealthDataServicesRuntimeConfiguration.cs | 2 + .../Registration/IFhirRuntimeConfiguration.cs | 5 + .../Controllers/FhirControllerTests.cs | 49 ++++++ ...tencyOverEfficiencyFilterAttributeTests.cs | 100 ++++++++++++ .../Headers/HttpContextExtensionsTests.cs | 153 ++++++++++++++++++ .../Bundle/BundleHandlerEdgeCaseTests.cs | 16 +- ...Health.Fhir.Shared.Api.UnitTests.projitems | 3 + .../Controllers/FhirController.cs | 13 +- ...eryLatencyOverEfficiencyFilterAttribute.cs | 61 +++++++ .../Features/Headers/HttpContextExtensions.cs | 43 +++-- .../Resources/Bundle/BundleHandler.cs | 15 +- .../Bundle/BundleHandlerStatistics.cs | 8 +- ...Microsoft.Health.Fhir.Shared.Api.projitems | 1 + .../Modules/FhirModule.cs | 1 + 16 files changed, 441 insertions(+), 32 deletions(-) create mode 100644 src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/FhirControllerTests.cs create mode 100644 src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/QueryLatencyOverEfficiencyFilterAttributeTests.cs create mode 100644 src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Headers/HttpContextExtensionsTests.cs create mode 100644 src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/QueryLatencyOverEfficiencyFilterAttribute.cs diff --git a/src/Microsoft.Health.Fhir.Core/Features/KnownHeaders.cs b/src/Microsoft.Health.Fhir.Core/Features/KnownHeaders.cs index d73ca2ae25..e289a6012a 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/KnownHeaders.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/KnownHeaders.cs @@ -30,6 +30,7 @@ public static class KnownHeaders public const string ProfileValidation = "x-ms-profile-validation"; public const string CustomAuditHeaderPrefix = "X-MS-AZUREFHIR-AUDIT-"; public const string FhirUserHeader = "x-ms-fhiruser"; + public const string QueryLatencyOverEfficiency = "x-ms-query-latency-over-efficiency"; // #conditionalQueryParallelism - Header used to activate parallel conditional-query processing. public const string ConditionalQueryProcessingLogic = "x-conditionalquery-processing-logic"; diff --git a/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs index ae46127ff4..51b29f5b9e 100644 --- a/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs +++ b/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs @@ -18,5 +18,7 @@ public class AzureApiForFhirRuntimeConfiguration : IFhirRuntimeConfiguration public bool IsCustomerKeyValidationBackgroundWorkerSupported => false; public bool IsTransactionSupported => false; + + public bool IsLatencyOverEfficiencySupported => true; } } diff --git a/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs index e59997b6d1..26ff55233a 100644 --- a/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs +++ b/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs @@ -18,5 +18,7 @@ public class AzureHealthDataServicesRuntimeConfiguration : IFhirRuntimeConfigura public bool IsCustomerKeyValidationBackgroundWorkerSupported => true; public bool IsTransactionSupported => true; + + public bool IsLatencyOverEfficiencySupported => false; } } diff --git a/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs index 30f6915637..94411f0c67 100644 --- a/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs +++ b/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs @@ -28,5 +28,10 @@ public interface IFhirRuntimeConfiguration /// Support to transactions. /// bool IsTransactionSupported { get; } + + /// + /// Supports the 'latency-over-efficiency' HTTP header. + /// + bool IsLatencyOverEfficiencySupported { get; } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/FhirControllerTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/FhirControllerTests.cs new file mode 100644 index 0000000000..97098237ce --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/FhirControllerTests.cs @@ -0,0 +1,49 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Health.Api.Features.Audit; +using Microsoft.Health.Fhir.Api.Controllers; +using Microsoft.Health.Fhir.Api.Features.Filters; +using Microsoft.Health.Fhir.Tests.Common; +using Microsoft.Health.Test.Utilities; +using Xunit; + +namespace Microsoft.Health.Fhir.Api.UnitTests.Controllers +{ + [Trait(Traits.OwningTeam, OwningTeam.Fhir)] + [Trait(Traits.Category, Categories.Web)] + public sealed class FhirControllerTests + { + [Fact] + public void WhenProviderAFhirController_CheckIfAllExpectedServiceFilterAttributesArePresent() + { + Type[] expectedCustomAttributes = new Type[] + { + typeof(AuditLoggingFilterAttribute), + typeof(OperationOutcomeExceptionFilterAttribute), + typeof(ValidateFormatParametersAttribute), + typeof(QueryLatencyOverEfficiencyFilterAttribute), + }; + + ServiceFilterAttribute[] serviceFilterAttributes = Attribute.GetCustomAttributes(typeof(FhirController), typeof(ServiceFilterAttribute)) + .Select(a => a as ServiceFilterAttribute) + .ToArray(); + + foreach (Type expectedCustomAttribute in expectedCustomAttributes) + { + bool attributeWasFound = serviceFilterAttributes.Any(s => s.ServiceType == expectedCustomAttribute); + + if (!attributeWasFound) + { + string errorMessage = $"The custom attribute '{expectedCustomAttribute.ToString()}' is not assigned to '{nameof(FhirController)}'."; + Assert.Fail(errorMessage); + } + } + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/QueryLatencyOverEfficiencyFilterAttributeTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/QueryLatencyOverEfficiencyFilterAttributeTests.cs new file mode 100644 index 0000000000..1af7df7ce1 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/QueryLatencyOverEfficiencyFilterAttributeTests.cs @@ -0,0 +1,100 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Microsoft.Health.Core.Features.Context; +using Microsoft.Health.Fhir.Api.Features.Filters; +using Microsoft.Health.Fhir.Core.Features; +using Microsoft.Health.Fhir.Core.Features.Context; +using Microsoft.Health.Fhir.Core.Registration; +using Microsoft.Health.Fhir.Core.UnitTests.Features.Context; +using Microsoft.Health.Fhir.Tests.Common; +using Microsoft.Health.Test.Utilities; +using NSubstitute; +using Xunit; + +namespace Microsoft.Health.Fhir.Api.UnitTests.Features.Filters +{ + [Trait(Traits.OwningTeam, OwningTeam.Fhir)] + [Trait(Traits.Category, Categories.Web)] + public sealed class QueryLatencyOverEfficiencyFilterAttributeTests + { + private readonly IFhirRuntimeConfiguration _azureApiForFhirConfiguration = new AzureApiForFhirRuntimeConfiguration(); + private readonly IFhirRuntimeConfiguration _azureHealthDataServicesFhirConfiguration = new AzureHealthDataServicesRuntimeConfiguration(); + + [Fact] + public void GivenAValidHttpContextForAzureApiForFhir_WhenItContainsALatencyOverEfficiencyFlag_ThenFhirContextIsDecorated() + { + var httpRequest = GetFakeHttpContext(isLatencyOverEfficiencyEnabled: true); + + var filter = new QueryLatencyOverEfficiencyFilterAttribute(httpRequest.RequestContext, _azureApiForFhirConfiguration); + filter.OnActionExecuting(httpRequest.ActionContext); + + var fhirContextPropertyBag = httpRequest.RequestContext.RequestContext.Properties; + + Assert.True(fhirContextPropertyBag.ContainsKey(KnownQueryParameterNames.OptimizeConcurrency)); + Assert.Equal(true, fhirContextPropertyBag[KnownQueryParameterNames.OptimizeConcurrency]); + } + + [Fact] + public void GivenAValidHttpContextForAzureHealthDataService_WhenItContainsALatencyOverEfficiencyFlag_ThenFhirContextIsNotDecorated() + { + // The latency-over-efficiency flag is only applicable to Azure API for FHIR. + + var httpRequest = GetFakeHttpContext(isLatencyOverEfficiencyEnabled: true); + + var filter = new QueryLatencyOverEfficiencyFilterAttribute(httpRequest.RequestContext, _azureHealthDataServicesFhirConfiguration); + filter.OnActionExecuting(httpRequest.ActionContext); + + var fhirContextPropertyBag = httpRequest.RequestContext.RequestContext.Properties; + + Assert.False(fhirContextPropertyBag.ContainsKey(KnownQueryParameterNames.OptimizeConcurrency)); + } + + [Fact] + public void GivenAValidHttpContext_WhenItDoesNotContainALatencyOverEfficiencyFlag_ThenFhirContextIsClean() + { + var httpRequest = GetFakeHttpContext(isLatencyOverEfficiencyEnabled: false); + + var filter = new QueryLatencyOverEfficiencyFilterAttribute(httpRequest.RequestContext, _azureApiForFhirConfiguration); + filter.OnActionExecuting(httpRequest.ActionContext); + + var fhirContextPropertyBag = httpRequest.RequestContext.RequestContext.Properties; + + Assert.False(fhirContextPropertyBag.ContainsKey(KnownQueryParameterNames.OptimizeConcurrency)); + } + + private static (RequestContextAccessor RequestContext, ActionExecutingContext ActionContext) GetFakeHttpContext(bool isLatencyOverEfficiencyEnabled) + { + var httpContext = new DefaultHttpContext(); + + if (isLatencyOverEfficiencyEnabled) + { + httpContext.Request.Headers[KnownHeaders.QueryLatencyOverEfficiency] = "true"; + } + + ActionExecutingContext context = new ActionExecutingContext( + new ActionContext( + httpContext, + new RouteData(), + new ActionDescriptor()), + new List(), + actionArguments: new Dictionary(), + FilterTestsHelper.CreateMockFhirController()); + + DefaultFhirRequestContext fhirRequestContext = new DefaultFhirRequestContext(); + + var fhirRequestContextAccessor = Substitute.For>(); + fhirRequestContextAccessor.RequestContext.Returns(fhirRequestContext); + + return (fhirRequestContextAccessor, context); + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Headers/HttpContextExtensionsTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Headers/HttpContextExtensionsTests.cs new file mode 100644 index 0000000000..365b85b730 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Headers/HttpContextExtensionsTests.cs @@ -0,0 +1,153 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.Health.Fhir.Api.Features.Headers; +using Microsoft.Health.Fhir.Api.Features.Resources; +using Microsoft.Health.Fhir.Api.Features.Resources.Bundle; +using Microsoft.Health.Fhir.Core.Features; +using Microsoft.Health.Fhir.Core.Features.Context; +using Microsoft.Health.Fhir.Core.Features.Persistence.Orchestration; +using Microsoft.Health.Fhir.Tests.Common; +using Microsoft.Health.Test.Utilities; +using Xunit; + +namespace Microsoft.Health.Fhir.Api.UnitTests.Features.Headers +{ + [Trait(Traits.OwningTeam, OwningTeam.Fhir)] + [Trait(Traits.Category, Categories.Web)] + public sealed class HttpContextExtensionsTests + { + [Fact] + public void WhenHttpContextDoesNotHaveCustomHeaders_ReturnDefaultValues() + { + HttpContext httpContext = GetFakeHttpContext(); + + bool isLatencyOverEfficiencyEnabled = httpContext.IsLatencyOverEfficiencyEnabled(); + Assert.False(isLatencyOverEfficiencyEnabled); + + BundleProcessingLogic bundleProcessingLogic = httpContext.GetBundleProcessingLogic(); + Assert.Equal(BundleProcessingLogic.Sequential, bundleProcessingLogic); + + // #conditionalQueryParallelism + ConditionalQueryProcessingLogic conditionalQueryProcessingLogic = httpContext.GetConditionalQueryProcessingLogic(); + Assert.Equal(ConditionalQueryProcessingLogic.Sequential, conditionalQueryProcessingLogic); + } + + [Theory] + [InlineData("", false)] + [InlineData(null, false)] + [InlineData("false", false)] + [InlineData("falsE", false)] + [InlineData("FALSE", false)] + [InlineData("2112", false)] + [InlineData("true", true)] + [InlineData("true ", true)] + [InlineData("TRUE", true)] + [InlineData(" TRUE ", true)] + [InlineData(" tRuE ", true)] + public void WhenHttpContextHasCustomHeaders_ReturnIfLatencyOverEfficiencyIsEnabled(string value, bool isEnabled) + { + var httpHeaders = new Dictionary() { { KnownHeaders.QueryLatencyOverEfficiency, value } }; + HttpContext httpContext = GetFakeHttpContext(httpHeaders); + + bool isLatencyOverEfficiencyEnabled = httpContext.IsLatencyOverEfficiencyEnabled(); + + Assert.Equal(isEnabled, isLatencyOverEfficiencyEnabled); + } + + [Theory] + [InlineData("", ConditionalQueryProcessingLogic.Sequential)] + [InlineData(null, ConditionalQueryProcessingLogic.Sequential)] + [InlineData("sequential", ConditionalQueryProcessingLogic.Sequential)] + [InlineData("sequential ", ConditionalQueryProcessingLogic.Sequential)] + [InlineData("Sequential", ConditionalQueryProcessingLogic.Sequential)] + [InlineData("2112", ConditionalQueryProcessingLogic.Sequential)] + [InlineData("red barchetta", ConditionalQueryProcessingLogic.Sequential)] + [InlineData("parallel", ConditionalQueryProcessingLogic.Parallel)] + [InlineData("parallel ", ConditionalQueryProcessingLogic.Parallel)] + [InlineData("Parallel", ConditionalQueryProcessingLogic.Parallel)] + [InlineData(" pArAllEl ", ConditionalQueryProcessingLogic.Parallel)] + [InlineData("PARALLEL", ConditionalQueryProcessingLogic.Parallel)] + public void WhenHttpContextHasCustomHeaders_ReturnIfConditionalQueryProcessingLogicIsSet(string value, ConditionalQueryProcessingLogic processingLogic) + { + // #conditionalQueryParallelism + + var httpHeaders = new Dictionary() { { KnownHeaders.ConditionalQueryProcessingLogic, value } }; + HttpContext httpContext = GetFakeHttpContext(httpHeaders); + + ConditionalQueryProcessingLogic conditionalQueryProcessingLogic = httpContext.GetConditionalQueryProcessingLogic(); + + Assert.Equal(processingLogic, conditionalQueryProcessingLogic); + } + + [Theory] + [InlineData("", BundleProcessingLogic.Sequential)] + [InlineData(null, BundleProcessingLogic.Sequential)] + [InlineData("sequential", BundleProcessingLogic.Sequential)] + [InlineData("sequential ", BundleProcessingLogic.Sequential)] + [InlineData("Sequential", BundleProcessingLogic.Sequential)] + [InlineData("2112", BundleProcessingLogic.Sequential)] + [InlineData("red barchetta", BundleProcessingLogic.Sequential)] + [InlineData("parallel", BundleProcessingLogic.Parallel)] + [InlineData("parallel ", BundleProcessingLogic.Parallel)] + [InlineData("Parallel", BundleProcessingLogic.Parallel)] + [InlineData(" pArAllEl ", BundleProcessingLogic.Parallel)] + [InlineData("PARALLEL", BundleProcessingLogic.Parallel)] + public void WhenHttpContextHasCustomHeaders_ReturnIfBundleProcessingLogicIsSet(string value, BundleProcessingLogic processingLogic) + { + var httpHeaders = new Dictionary() { { BundleOrchestratorNamingConventions.HttpHeaderBundleProcessingLogic, value } }; + HttpContext httpContext = GetFakeHttpContext(httpHeaders); + + BundleProcessingLogic bundleProcessingLogic = httpContext.GetBundleProcessingLogic(); + + Assert.Equal(processingLogic, bundleProcessingLogic); + } + + [Fact] + public void WhenProvidedAFhirRequestContext_ThenDecorateItWithOptimizeConcurrency() + { + // #conditionalQueryParallelism + + IFhirRequestContext fhirRequestContext = new Core.UnitTests.Features.Context.DefaultFhirRequestContext() + { + BaseUri = new Uri("https://localhost/"), + CorrelationId = Guid.NewGuid().ToString(), + ResponseHeaders = new HeaderDictionary(), + RequestHeaders = new HeaderDictionary(), + }; + + fhirRequestContext.DecorateRequestContextWithOptimizedConcurrency(); + + Assert.True(fhirRequestContext.Properties.ContainsKey(KnownQueryParameterNames.OptimizeConcurrency)); + Assert.Equal(true, fhirRequestContext.Properties[KnownQueryParameterNames.OptimizeConcurrency]); + } + + private static HttpContext GetFakeHttpContext(IReadOnlyDictionary optionalHttpHeaders = default) + { + var httpContext = new DefaultHttpContext() + { + Request = + { + Scheme = "https", + Host = new HostString("localhost"), + PathBase = new PathString("/"), + }, + }; + + if (optionalHttpHeaders != null) + { + foreach (var header in optionalHttpHeaders) + { + httpContext.Request.Headers.Append(header.Key, header.Value); + } + } + + return httpContext; + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Resources/Bundle/BundleHandlerEdgeCaseTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Resources/Bundle/BundleHandlerEdgeCaseTests.cs index 11e010672a..4a8c937d9e 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Resources/Bundle/BundleHandlerEdgeCaseTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Resources/Bundle/BundleHandlerEdgeCaseTests.cs @@ -11,6 +11,7 @@ using System.IO; using System.Linq; using System.Threading; +using Azure; using Hl7.Fhir.Model; using Hl7.Fhir.Serialization; using Hl7.Fhir.Specification.Navigation; @@ -87,9 +88,9 @@ public void GivenABundle_WhenProcessedWithConditionalQueryMaxParallelism_TheFhir // 4 - If the created HTTP request does not contain the header "x-conditionalquery-processing-logic" set as "parallel", then the key "_optimizeConcurrency" // is not expected in the FHIR Request Context property bag. - var bundleHandlerComponents = GetBundleHandlerComponents(new BundleRequestOptions() { MaxParallelism = maxParallelism }); + var requestContext = CreateRequestContextForBundleHandlerProcessing(new BundleRequestOptions() { MaxParallelism = maxParallelism }); - var fhirContextPropertyBag = bundleHandlerComponents.FhirRequestContext.Properties; + var fhirContextPropertyBag = requestContext.Properties; if (maxParallelism) { @@ -102,7 +103,7 @@ public void GivenABundle_WhenProcessedWithConditionalQueryMaxParallelism_TheFhir } } - private (IRouter Router, BundleConfiguration BundleConfiguration, IMediator Mediator, BundleHandler BundleHandler, IFhirRequestContext FhirRequestContext) GetBundleHandlerComponents(BundleRequestOptions options) + private IFhirRequestContext CreateRequestContextForBundleHandlerProcessing(BundleRequestOptions options) { IRouter router = Substitute.For(); @@ -147,6 +148,11 @@ public void GivenABundle_WhenProcessedWithConditionalQueryMaxParallelism_TheFhir httpContext.Request.Headers[KnownHeaders.ConditionalQueryProcessingLogic] = new StringValues("parallel"); } + if (options.QueryLatencyOverEfficiency) + { + httpContext.Request.Headers[KnownHeaders.QueryLatencyOverEfficiency] = new StringValues("true"); + } + httpContextAccessor.HttpContext.Returns(httpContext); var transactionHandler = Substitute.For(); @@ -174,7 +180,7 @@ public void GivenABundle_WhenProcessedWithConditionalQueryMaxParallelism_TheFhir mediator, NullLogger.Instance); - return (router, bundleConfiguration, mediator, bundleHandler, fhirRequestContextAccessor.RequestContext); + return fhirRequestContextAccessor.RequestContext; } private IFeatureCollection CreateFeatureCollection(IRouter router) @@ -214,6 +220,8 @@ private IFeatureCollection CreateFeatureCollection(IRouter router) private sealed class BundleRequestOptions() { public bool MaxParallelism { get; set; } = false; + + public bool QueryLatencyOverEfficiency { get; set; } = false; } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems index e1e051edbf..d95f4f111d 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems @@ -13,6 +13,7 @@ + @@ -34,6 +35,7 @@ + @@ -51,6 +53,7 @@ + diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs index a662da0c91..d2bb4d821f 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs @@ -59,6 +59,7 @@ namespace Microsoft.Health.Fhir.Api.Controllers [ServiceFilter(typeof(AuditLoggingFilterAttribute))] [ServiceFilter(typeof(OperationOutcomeExceptionFilterAttribute))] [ServiceFilter(typeof(ValidateFormatParametersAttribute))] + [ServiceFilter(typeof(QueryLatencyOverEfficiencyFilterAttribute))] [ValidateResourceTypeFilter] [ValidateModelState] public class FhirController : Controller @@ -181,7 +182,7 @@ public async Task ConditionalCreate([FromBody] Resource resource) { StringValues conditionalCreateHeader = HttpContext.Request.Headers[KnownHeaders.IfNoneExist]; - SetupRequestContextWithConditionalQueryMaxParallelism(); + SetupConditionalRequestWithQueryOptimizeConcurrency(); Tuple[] conditionalParameters = QueryHelpers.ParseQuery(conditionalCreateHeader) .SelectMany(query => query.Value, (query, value) => Tuple.Create(query.Key, value)).ToArray(); @@ -230,7 +231,7 @@ public async Task Update([FromBody] Resource resource, [ModelBind [AuditEventType(AuditEventSubType.ConditionalUpdate)] public async Task ConditionalUpdate([FromBody] Resource resource) { - SetupRequestContextWithConditionalQueryMaxParallelism(); + SetupConditionalRequestWithQueryOptimizeConcurrency(); IReadOnlyList> conditionalParameters = GetQueriesForSearch(); @@ -435,7 +436,7 @@ public async Task ConditionalDelete(string typeParameter, [FromQu { IReadOnlyList> conditionalParameters = GetQueriesForSearch(); - SetupRequestContextWithConditionalQueryMaxParallelism(); + SetupConditionalRequestWithQueryOptimizeConcurrency(); DeleteResourceResponse response = await _mediator.Send( new ConditionalDeleteResourceRequest( @@ -496,7 +497,7 @@ public async Task ConditionalPatchJson(string typeParameter, [Fro IReadOnlyList> conditionalParameters = GetQueriesForSearch(); var payload = new JsonPatchPayload(patchDocument); - SetupRequestContextWithConditionalQueryMaxParallelism(); + SetupConditionalRequestWithQueryOptimizeConcurrency(); UpsertResourceResponse response = await _mediator.ConditionalPatchResourceAsync( new ConditionalPatchResourceRequest(typeParameter, payload, conditionalParameters, GetBundleResourceContext(), ifMatchHeader), @@ -541,7 +542,7 @@ public async Task ConditionalPatchFhir(string typeParameter, [Fro IReadOnlyList> conditionalParameters = GetQueriesForSearch(); var payload = new FhirPathPatchPayload(paramsResource); - SetupRequestContextWithConditionalQueryMaxParallelism(); + SetupConditionalRequestWithQueryOptimizeConcurrency(); UpsertResourceResponse response = await _mediator.ConditionalPatchResourceAsync( new ConditionalPatchResourceRequest(typeParameter, payload, conditionalParameters, GetBundleResourceContext(), ifMatchHeader), @@ -690,7 +691,7 @@ private BundleResourceContext GetBundleResourceContext() return null; } - private void SetupRequestContextWithConditionalQueryMaxParallelism() + private void SetupConditionalRequestWithQueryOptimizeConcurrency() { if (HttpContext?.Request?.Headers != null && _fhirRequestContextAccessor != null) { diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/QueryLatencyOverEfficiencyFilterAttribute.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/QueryLatencyOverEfficiencyFilterAttribute.cs new file mode 100644 index 0000000000..51bef05f08 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/QueryLatencyOverEfficiencyFilterAttribute.cs @@ -0,0 +1,61 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using EnsureThat; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Health.Core.Features.Context; +using Microsoft.Health.Fhir.Api.Features.Headers; +using Microsoft.Health.Fhir.Core.Features.Context; +using Microsoft.Health.Fhir.Core.Registration; + +namespace Microsoft.Health.Fhir.Api.Features.Filters +{ + /// + /// Latency over efficiency filter. + /// Adds to FHIR Request Context a flag to optimize query latency over efficiency. + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class QueryLatencyOverEfficiencyFilterAttribute : ActionFilterAttribute + { + private readonly RequestContextAccessor _fhirRequestContextAccessor; + private readonly IFhirRuntimeConfiguration _runtimeConfiguration; + + public QueryLatencyOverEfficiencyFilterAttribute(RequestContextAccessor fhirRequestContextAccessor, IFhirRuntimeConfiguration runtimeConfiguration) + { + EnsureArg.IsNotNull(fhirRequestContextAccessor, nameof(fhirRequestContextAccessor)); + EnsureArg.IsNotNull(runtimeConfiguration, nameof(runtimeConfiguration)); + + _fhirRequestContextAccessor = fhirRequestContextAccessor; + _runtimeConfiguration = runtimeConfiguration; + } + + public override void OnActionExecuting(ActionExecutingContext context) + { + EnsureArg.IsNotNull(context, nameof(context)); + + if (_runtimeConfiguration.IsLatencyOverEfficiencySupported) + { + SetupConditionalRequestWithQueryOptimizeConcurrency(context.HttpContext, _fhirRequestContextAccessor.RequestContext); + } + + base.OnActionExecuting(context); + } + + private static void SetupConditionalRequestWithQueryOptimizeConcurrency(HttpContext context, IFhirRequestContext fhirRequestContext) + { + if (context?.Request?.Headers != null && fhirRequestContext != null) + { + bool latencyOverEfficiencyEnabled = context.IsLatencyOverEfficiencyEnabled(); + + if (latencyOverEfficiencyEnabled) + { + fhirRequestContext.DecorateRequestContextWithOptimizedConcurrency(); + } + } + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Headers/HttpContextExtensions.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Headers/HttpContextExtensions.cs index 11766b6ebd..f0e24a4b68 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Headers/HttpContextExtensions.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Headers/HttpContextExtensions.cs @@ -19,19 +19,19 @@ namespace Microsoft.Health.Fhir.Api.Features.Headers public static class HttpContextExtensions { /// - /// Retrieves from the HTTP header information about the conditional-query processing logic to be adopted. + /// Retrieves from the HTTP header if "Latency over efficiency" is enabled. /// /// HTTP context - public static ConditionalQueryProcessingLogic GetConditionalQueryProcessingLogic(this HttpContext outerHttpContext) + public static bool IsLatencyOverEfficiencyEnabled(this HttpContext outerHttpContext) { - var defaultValue = ConditionalQueryProcessingLogic.Sequential; + const bool defaultValue = false; if (outerHttpContext == null) { return defaultValue; } - if (outerHttpContext.Request.Headers.TryGetValue(KnownHeaders.ConditionalQueryProcessingLogic, out StringValues headerValues)) + if (outerHttpContext.Request.Headers.TryGetValue(KnownHeaders.QueryLatencyOverEfficiency, out StringValues headerValues)) { string processingLogicAsString = headerValues.FirstOrDefault(); if (string.IsNullOrWhiteSpace(processingLogicAsString)) @@ -39,27 +39,48 @@ public static ConditionalQueryProcessingLogic GetConditionalQueryProcessingLogic return defaultValue; } - ConditionalQueryProcessingLogic processingLogic = (ConditionalQueryProcessingLogic)Enum.Parse(typeof(ConditionalQueryProcessingLogic), processingLogicAsString.Trim(), ignoreCase: true); - return processingLogic; + if (bool.TryParse(headerValues.ToString().Trim(), out bool result)) + { + return result; + } } return defaultValue; } + /// + /// Retrieves from the HTTP header information about the conditional-query processing logic to be adopted. + /// + /// HTTP context + public static ConditionalQueryProcessingLogic GetConditionalQueryProcessingLogic(this HttpContext outerHttpContext) + { + return ExtractEnumerationFlagFromHttpHeader( + outerHttpContext, + httpHeaderName: KnownHeaders.ConditionalQueryProcessingLogic, + defaultValue: ConditionalQueryProcessingLogic.Sequential); + } + /// /// Retrieves from the HTTP header information about the bundle processing logic to be adopted. /// /// HTTP context public static BundleProcessingLogic GetBundleProcessingLogic(this HttpContext outerHttpContext) { - var defaultValue = BundleProcessingLogic.Sequential; + return ExtractEnumerationFlagFromHttpHeader( + outerHttpContext, + httpHeaderName: BundleOrchestratorNamingConventions.HttpHeaderBundleProcessingLogic, + defaultValue: BundleProcessingLogic.Sequential); + } + public static TEnum ExtractEnumerationFlagFromHttpHeader(HttpContext outerHttpContext, string httpHeaderName, TEnum defaultValue) + where TEnum : struct, Enum + { if (outerHttpContext == null) { return defaultValue; } - if (outerHttpContext.Request.Headers.TryGetValue(BundleOrchestratorNamingConventions.HttpHeaderBundleProcessingLogic, out StringValues headerValues)) + if (outerHttpContext.Request.Headers.TryGetValue(httpHeaderName, out StringValues headerValues)) { string processingLogicAsString = headerValues.FirstOrDefault(); if (string.IsNullOrWhiteSpace(processingLogicAsString)) @@ -67,8 +88,10 @@ public static BundleProcessingLogic GetBundleProcessingLogic(this HttpContext ou return defaultValue; } - BundleProcessingLogic processingLogic = (BundleProcessingLogic)Enum.Parse(typeof(BundleProcessingLogic), processingLogicAsString.Trim(), ignoreCase: true); - return processingLogic; + if (Enum.TryParse(processingLogicAsString.Trim(), ignoreCase: true, out TEnum result) && Enum.IsDefined(result)) + { + return result; + } } return defaultValue; diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandler.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandler.cs index 0e45425f7a..33ca37b553 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandler.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandler.cs @@ -85,7 +85,7 @@ public partial class BundleHandler : IRequestHandler Handle(BundleRequest request, CancellationToken cancellationToken) @@ -243,7 +243,7 @@ public async Task Handle(BundleRequest request, CancellationToke } } - private static ConditionalQueryProcessingLogic SetRequestContextWithConditionalQueryProcessingLogic(HttpContext outerHttpContext, IFhirRequestContext fhirRequestContext, ILogger logger) + private static bool SetRequestContextWithOptimizedQuerying(HttpContext outerHttpContext, IFhirRequestContext fhirRequestContext, ILogger logger) { try { @@ -252,16 +252,15 @@ private static ConditionalQueryProcessingLogic SetRequestContextWithConditionalQ if (conditionalQueryProcessingLogic == ConditionalQueryProcessingLogic.Parallel) { fhirRequestContext.DecorateRequestContextWithOptimizedConcurrency(); + return true; } - - return conditionalQueryProcessingLogic; } catch (Exception e) { logger.LogWarning(e, "Error while extracting the Conditional-Query Processing Logic out of the HTTP Header: {ErrorMessage}", e.Message); } - return ConditionalQueryProcessingLogic.Sequential; + return false; } private BundleProcessingLogic GetBundleProcessingLogic(HttpContext outerHttpContext, ILogger logger) @@ -921,7 +920,7 @@ private static OperationOutcome CreateOperationOutcome(OperationOutcome.IssueSev private BundleHandlerStatistics CreateNewBundleHandlerStatistics(BundleProcessingLogic processingLogic) { - BundleHandlerStatistics statistics = new BundleHandlerStatistics(_bundleType, processingLogic, _conditionalQueryProcessingLogic, _requestCount); + BundleHandlerStatistics statistics = new BundleHandlerStatistics(_bundleType, processingLogic, _optimizedQuerySet, _requestCount); statistics.StartCollectingResults(); diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerStatistics.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerStatistics.cs index 0805034d72..5e72ab2e89 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerStatistics.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerStatistics.cs @@ -24,13 +24,13 @@ internal sealed class BundleHandlerStatistics : BaseOperationStatistics public BundleHandlerStatistics( BundleType? bundleType, BundleProcessingLogic bundleProcessingLogic, - ConditionalQueryProcessingLogic conditionalQueryProcessingLogic, + bool optimizedQuerySet, int numberOfResources) : base() { BundleType = bundleType; BundleProcessingLogic = bundleProcessingLogic; - ConditionalQueryProcessingLogic = conditionalQueryProcessingLogic; + OptimizedQueryProcessing = optimizedQuerySet; NumberOfResources = numberOfResources; _entries = new List(); } @@ -41,7 +41,7 @@ public BundleHandlerStatistics( public BundleProcessingLogic BundleProcessingLogic { get; } - public ConditionalQueryProcessingLogic ConditionalQueryProcessingLogic { get; } + public bool OptimizedQueryProcessing { get; } public override string GetLoggingCategory() => LoggingCategory; @@ -61,7 +61,7 @@ public override string GetStatisticsAsJson() label = GetLoggingCategory(), bundleType = BundleType.ToString(), processingLogic = BundleProcessingLogic.ToString(), - conditionalQuery = ConditionalQueryProcessingLogic.ToString(), + optimizedQuerySet = OptimizedQueryProcessing.ToString(), numberOfResources = NumberOfResources, executionTime = ElapsedMilliseconds, success = successedRequests, diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems b/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems index 00242a0448..61ba295b49 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems +++ b/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems @@ -26,6 +26,7 @@ + diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs b/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs index 03f6bb1f77..fc262bb3f0 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs @@ -108,6 +108,7 @@ ResourceElement SetMetadata(Resource resource, string versionId, DateTimeOffset services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Support for resolve() FhirPathCompiler.DefaultSymbolTable.AddFhirExtensions();