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