From 39b189886bda04dbc058ad9d294b2c0962f264d1 Mon Sep 17 00:00:00 2001 From: Robert Johnson Date: Fri, 24 Jan 2025 17:53:57 -0500 Subject: [PATCH] Add SQL performance option (#4785) --- .../ActionResults/ResourceActionResult.cs | 2 +- .../TooManyRequestsActionResult.cs | 2 +- .../Filters/QueryCacheFilterAttribute.cs | 61 +++++++++++++++++++ .../ValidateAsyncRequestFilterAttribute.cs | 2 +- .../ValidateExportRequestFilterAttribute.cs | 10 +-- .../ValidateFormatParametersAttribute.cs | 6 +- .../ValidateImportRequestFilterAttribute.cs | 4 +- .../ValidateParametersResourceAttribute.cs | 4 +- .../ValidateReindexRequestFilterAttribute.cs | 2 +- .../Features/Headers/HttpContextExtensions.cs | 25 ++++++++ .../Import/InitialImportLockMiddleware.cs | 2 +- .../ConditionalQueryProcessingLogic.cs | 0 .../Routing/SearchPostReroutingMiddleware.cs | 2 +- .../Features/Routing/UrlResolver.cs | 4 +- .../SMART/SmartClinicalScopesMiddleware.cs | 4 +- .../Throttling/ThrottlingMiddleware.cs | 4 +- .../Features/KnownHeaders.cs | 2 + .../Features/KnownQueryParameterNames.cs | 10 ++- .../AzureApiForFhirRuntimeConfiguration.cs | 2 + ...eHealthDataServicesRuntimeConfiguration.cs | 2 + .../Registration/IFhirRuntimeConfiguration.cs | 5 ++ .../Controllers/FhirController.cs | 1 + ...Microsoft.Health.Fhir.Shared.Api.projitems | 2 - .../Modules/FhirModule.cs | 1 + .../Features/Search/QueryCacheSetting.cs | 15 +++++ .../Features/Search/SqlServerSearchService.cs | 56 +++++++++++++++-- 26 files changed, 198 insertions(+), 32 deletions(-) create mode 100644 src/Microsoft.Health.Fhir.Api/Features/Filters/QueryCacheFilterAttribute.cs rename src/{Microsoft.Health.Fhir.Shared.Api => Microsoft.Health.Fhir.Api}/Features/Headers/HttpContextExtensions.cs (82%) rename src/{Microsoft.Health.Fhir.Shared.Api => Microsoft.Health.Fhir.Api}/Features/Resources/ConditionalQueryProcessingLogic.cs (100%) create mode 100644 src/Microsoft.Health.Fhir.SqlServer/Features/Search/QueryCacheSetting.cs diff --git a/src/Microsoft.Health.Fhir.Api/Features/ActionResults/ResourceActionResult.cs b/src/Microsoft.Health.Fhir.Api/Features/ActionResults/ResourceActionResult.cs index ae187d7557..5c37edf93b 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/ActionResults/ResourceActionResult.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/ActionResults/ResourceActionResult.cs @@ -65,7 +65,7 @@ public override async Task ExecuteResultAsync(ActionContext context) } catch (ObjectDisposedException ode) { - throw new ServiceUnavailableException(Resources.NotAbleToCreateTheFinalResultsOfAnOperation, ode); + throw new ServiceUnavailableException(Api.Resources.NotAbleToCreateTheFinalResultsOfAnOperation, ode); } HttpResponse response = context.HttpContext.Response; diff --git a/src/Microsoft.Health.Fhir.Api/Features/ActionResults/TooManyRequestsActionResult.cs b/src/Microsoft.Health.Fhir.Api/Features/ActionResults/TooManyRequestsActionResult.cs index 28bc594f42..5ea3122906 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/ActionResults/TooManyRequestsActionResult.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/ActionResults/TooManyRequestsActionResult.cs @@ -17,7 +17,7 @@ public TooManyRequestsActionResult() new OperationOutcomeIssue( OperationOutcomeConstants.IssueSeverity.Error, OperationOutcomeConstants.IssueType.Throttled, - Resources.TooManyConcurrentRequests), + Api.Resources.TooManyConcurrentRequests), HttpStatusCode.TooManyRequests) { } diff --git a/src/Microsoft.Health.Fhir.Api/Features/Filters/QueryCacheFilterAttribute.cs b/src/Microsoft.Health.Fhir.Api/Features/Filters/QueryCacheFilterAttribute.cs new file mode 100644 index 0000000000..25a50e5047 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Api/Features/Filters/QueryCacheFilterAttribute.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 QueryCacheFilterAttribute : ActionFilterAttribute + { + private readonly RequestContextAccessor _fhirRequestContextAccessor; + private readonly IFhirRuntimeConfiguration _runtimeConfiguration; + + public QueryCacheFilterAttribute(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.IsQueryCacheSupported) + { + SetupConditionalRequestWithQueryCache(context.HttpContext, _fhirRequestContextAccessor.RequestContext); + } + + base.OnActionExecuting(context); + } + + private static void SetupConditionalRequestWithQueryCache(HttpContext context, IFhirRequestContext fhirRequestContext) + { + if (context?.Request?.Headers != null && fhirRequestContext != null) + { + string useQueryCache = context.GetQueryCache(); + + if (!string.IsNullOrEmpty(useQueryCache)) + { + fhirRequestContext.DecorateRequestContextWithQueryCache(useQueryCache); + } + } + } + } +} diff --git a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateAsyncRequestFilterAttribute.cs b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateAsyncRequestFilterAttribute.cs index 1ebf91e912..2d23c81d1d 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateAsyncRequestFilterAttribute.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateAsyncRequestFilterAttribute.cs @@ -28,7 +28,7 @@ public override void OnActionExecuting(ActionExecutingContext context) if (!context.HttpContext.Request.Headers.TryGetValue(KnownHeaders.Prefer, out var preferHeaderValue) || !string.Equals(preferHeaderValue[0], PreferHeaderExpectedValue, StringComparison.OrdinalIgnoreCase)) { - throw new RequestNotValidException(string.Format(Resources.UnsupportedHeaderValue, preferHeaderValue.FirstOrDefault(), KnownHeaders.Prefer)); + throw new RequestNotValidException(string.Format(Api.Resources.UnsupportedHeaderValue, preferHeaderValue.FirstOrDefault(), KnownHeaders.Prefer)); } } } diff --git a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateExportRequestFilterAttribute.cs b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateExportRequestFilterAttribute.cs index 77db0a8bd2..8c18b3d2f1 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateExportRequestFilterAttribute.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateExportRequestFilterAttribute.cs @@ -65,7 +65,7 @@ public override void OnActionExecuting(ActionExecutingContext context) acceptHeaderValue.Count != 1 || !string.Equals(acceptHeaderValue[0], KnownContentTypes.JsonContentType, StringComparison.OrdinalIgnoreCase)) { - throw new RequestNotValidException(string.Format(Resources.UnsupportedHeaderValue, acceptHeaderValue.FirstOrDefault(), HeaderNames.Accept)); + throw new RequestNotValidException(string.Format(Api.Resources.UnsupportedHeaderValue, acceptHeaderValue.FirstOrDefault(), HeaderNames.Accept)); } if (context.HttpContext.Request.Headers.TryGetValue(PreferHeaderName, out var preferHeaderValues)) @@ -79,7 +79,7 @@ public override void OnActionExecuting(ActionExecutingContext context) || (v.Length == 1 && !(requiredHeaderValueFound = string.Equals(v[0], PreferHeaderValueRequired, StringComparison.OrdinalIgnoreCase))) || (v.Length == 2 && (!string.Equals(v[0], PreferHeaderValueOptional, StringComparison.OrdinalIgnoreCase) || !Enum.TryParse(v[1], true, out _)))) { - throw new RequestNotValidException(string.Format(Resources.UnsupportedHeaderValue, value, PreferHeaderName)); + throw new RequestNotValidException(string.Format(Api.Resources.UnsupportedHeaderValue, value, PreferHeaderName)); } } @@ -102,14 +102,14 @@ public override void OnActionExecuting(ActionExecutingContext context) continue; } - throw new RequestNotValidException(string.Format(Resources.UnsupportedParameter, paramName)); + throw new RequestNotValidException(string.Format(Api.Resources.UnsupportedParameter, paramName)); } if (queryCollection?.Keys != null && queryCollection.Keys.Contains(KnownQueryParameterNames.TypeFilter) && !queryCollection.Keys.Contains(KnownQueryParameterNames.Type)) { - throw new RequestNotValidException(Resources.TypeFilterWithoutTypeIsUnsupported); + throw new RequestNotValidException(Api.Resources.TypeFilterWithoutTypeIsUnsupported); } if (queryCollection.TryGetValue(KnownQueryParameterNames.OutputFormat, out var outputFormats)) @@ -118,7 +118,7 @@ public override void OnActionExecuting(ActionExecutingContext context) { if (!(outputFormat == null || SupportedOutputFormats.Contains(outputFormat))) { - throw new RequestNotValidException(string.Format(Resources.InvalidOutputFormat, outputFormat)); + throw new RequestNotValidException(string.Format(Api.Resources.InvalidOutputFormat, outputFormat)); } } } diff --git a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateFormatParametersAttribute.cs b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateFormatParametersAttribute.cs index dc51d2f779..e9af338d63 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateFormatParametersAttribute.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateFormatParametersAttribute.cs @@ -51,13 +51,13 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context { if (!await _parametersValidator.IsFormatSupportedAsync(headerValue[0])) { - throw new UnsupportedMediaTypeException(string.Format(Resources.UnsupportedHeaderValue, headerValue.FirstOrDefault(), HeaderNames.ContentType)); + throw new UnsupportedMediaTypeException(string.Format(Api.Resources.UnsupportedHeaderValue, headerValue.FirstOrDefault(), HeaderNames.ContentType)); } } else { // If no content type is supplied, then the server should respond with an unsupported media type exception. - throw new UnsupportedMediaTypeException(Resources.ContentTypeHeaderRequired); + throw new UnsupportedMediaTypeException(Api.Resources.ContentTypeHeaderRequired); } } else if (httpContext.Request.Method.Equals(HttpMethod.Patch.Method, StringComparison.OrdinalIgnoreCase)) @@ -66,7 +66,7 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context { if (!await _parametersValidator.IsPatchFormatSupportedAsync(headerValue[0])) { - throw new UnsupportedMediaTypeException(string.Format(Resources.UnsupportedHeaderValue, headerValue.FirstOrDefault(), HeaderNames.ContentType)); + throw new UnsupportedMediaTypeException(string.Format(Api.Resources.UnsupportedHeaderValue, headerValue.FirstOrDefault(), HeaderNames.ContentType)); } } } diff --git a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateImportRequestFilterAttribute.cs b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateImportRequestFilterAttribute.cs index c4146c06a6..3d31306e25 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateImportRequestFilterAttribute.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateImportRequestFilterAttribute.cs @@ -35,7 +35,7 @@ public override void OnActionExecuting(ActionExecutingContext context) preferHeaderValue.Count != 1 || !string.Equals(preferHeaderValue[0], PreferHeaderExpectedValue, StringComparison.OrdinalIgnoreCase)) { - throw new RequestNotValidException(string.Format(Resources.UnsupportedHeaderValue, preferHeaderValue.FirstOrDefault(), PreferHeaderName)); + throw new RequestNotValidException(string.Format(Api.Resources.UnsupportedHeaderValue, preferHeaderValue.FirstOrDefault(), PreferHeaderName)); } if (string.Equals(context.HttpContext.Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) @@ -44,7 +44,7 @@ public override void OnActionExecuting(ActionExecutingContext context) contentTypeHeaderValue.Count != 1 || !contentTypeHeaderValue[0].Contains(ContentTypeHeaderExpectedValue, StringComparison.OrdinalIgnoreCase)) { - throw new RequestNotValidException(string.Format(Resources.UnsupportedHeaderValue, contentTypeHeaderValue.FirstOrDefault(), HeaderNames.ContentType)); + throw new RequestNotValidException(string.Format(Api.Resources.UnsupportedHeaderValue, contentTypeHeaderValue.FirstOrDefault(), HeaderNames.ContentType)); } } } diff --git a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateParametersResourceAttribute.cs b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateParametersResourceAttribute.cs index 9b08cf1095..49020c7b16 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateParametersResourceAttribute.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateParametersResourceAttribute.cs @@ -25,12 +25,12 @@ public override void OnActionExecuting(ActionExecutingContext context) context.ActionArguments?.TryGetValue("inputParams", out inputResource); if (inputResource == null) { - throw new RequestNotValidException(Resources.MissingInputParams); + throw new RequestNotValidException(Api.Resources.MissingInputParams); } if (inputResource is not Parameters) { - throw new RequestNotValidException(string.Format(Resources.UnsupportedResourceType, inputResource.GetType().ToString())); + throw new RequestNotValidException(string.Format(Api.Resources.UnsupportedResourceType, inputResource.GetType().ToString())); } } } diff --git a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateReindexRequestFilterAttribute.cs b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateReindexRequestFilterAttribute.cs index b19c62c444..d257de16fc 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateReindexRequestFilterAttribute.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateReindexRequestFilterAttribute.cs @@ -28,7 +28,7 @@ public override void OnActionExecuting(ActionExecutingContext context) if (context.HttpContext.Request.Headers.TryGetValue(PreferHeaderName, out var preferHeaderValue) && !string.Equals(preferHeaderValue[0], PreferHeaderExpectedValue, StringComparison.OrdinalIgnoreCase)) { - throw new RequestNotValidException(string.Format(Resources.UnsupportedHeaderValue, preferHeaderValue.FirstOrDefault(), PreferHeaderName)); + throw new RequestNotValidException(string.Format(Api.Resources.UnsupportedHeaderValue, preferHeaderValue.FirstOrDefault(), PreferHeaderName)); } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Headers/HttpContextExtensions.cs b/src/Microsoft.Health.Fhir.Api/Features/Headers/HttpContextExtensions.cs similarity index 82% rename from src/Microsoft.Health.Fhir.Shared.Api/Features/Headers/HttpContextExtensions.cs rename to src/Microsoft.Health.Fhir.Api/Features/Headers/HttpContextExtensions.cs index ba6de951ed..ff390aa122 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Headers/HttpContextExtensions.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Headers/HttpContextExtensions.cs @@ -48,6 +48,21 @@ public static bool IsLatencyOverEfficiencyEnabled(this HttpContext outerHttpCont return defaultValue; } + /// + /// Retrieves from the HTTP header information on using query caching. + /// + /// HTTP context + /// Query cache header value + public static string GetQueryCache(this HttpContext outerHttpContext) + { + if (outerHttpContext != null && outerHttpContext.Request.Headers.TryGetValue(KnownHeaders.QueryCacheSetting, out StringValues headerValues)) + { + return headerValues.FirstOrDefault(); + } + + return null; + } + /// /// Retrieves from the HTTP header information about the conditional-query processing logic to be adopted. /// @@ -110,5 +125,15 @@ public static bool DecorateRequestContextWithOptimizedConcurrency(this IFhirRequ return requestContext.Properties.TryAdd(KnownQueryParameterNames.OptimizeConcurrency, true); } + + public static bool DecorateRequestContextWithQueryCache(this IFhirRequestContext requestContext, string value) + { + if (requestContext == null) + { + return false; + } + + return requestContext.Properties.TryAdd(KnownQueryParameterNames.QueryCaching, value); + } } } diff --git a/src/Microsoft.Health.Fhir.Api/Features/Operations/Import/InitialImportLockMiddleware.cs b/src/Microsoft.Health.Fhir.Api/Features/Operations/Import/InitialImportLockMiddleware.cs index c40d8af662..ed99f2f169 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Operations/Import/InitialImportLockMiddleware.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Operations/Import/InitialImportLockMiddleware.cs @@ -26,7 +26,7 @@ public sealed class InitialImportLockMiddleware // hard-coding these to minimize resource consumption for locked message private const string LockedContentType = "application/json; charset=utf-8"; - private static readonly ReadOnlyMemory _lockedBody = CreateLockedBody(Resources.LockedForInitialImportMode); + private static readonly ReadOnlyMemory _lockedBody = CreateLockedBody(Api.Resources.LockedForInitialImportMode); public InitialImportLockMiddleware( RequestDelegate next, diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/ConditionalQueryProcessingLogic.cs b/src/Microsoft.Health.Fhir.Api/Features/Resources/ConditionalQueryProcessingLogic.cs similarity index 100% rename from src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/ConditionalQueryProcessingLogic.cs rename to src/Microsoft.Health.Fhir.Api/Features/Resources/ConditionalQueryProcessingLogic.cs diff --git a/src/Microsoft.Health.Fhir.Api/Features/Routing/SearchPostReroutingMiddleware.cs b/src/Microsoft.Health.Fhir.Api/Features/Routing/SearchPostReroutingMiddleware.cs index a3c01667a4..2e11c8bb50 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Routing/SearchPostReroutingMiddleware.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Routing/SearchPostReroutingMiddleware.cs @@ -50,7 +50,7 @@ public async Task Invoke(HttpContext context) { context.Response.Clear(); context.Response.StatusCode = (int)HttpStatusCode.BadRequest; - await context.Response.WriteAsync(Resources.ContentTypeFormUrlEncodedExpected); + await context.Response.WriteAsync(Api.Resources.ContentTypeFormUrlEncodedExpected); return; } } diff --git a/src/Microsoft.Health.Fhir.Api/Features/Routing/UrlResolver.cs b/src/Microsoft.Health.Fhir.Api/Features/Routing/UrlResolver.cs index c2ea11f4b9..3c0fb0b6c4 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Routing/UrlResolver.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Routing/UrlResolver.cs @@ -260,7 +260,7 @@ public Uri ResolveOperationResultUrl(string operationName, string id) routeName = RouteNames.GetBulkDeleteStatusById; break; default: - throw new OperationNotImplementedException(string.Format(Resources.OperationNotImplemented, operationName)); + throw new OperationNotImplementedException(string.Format(Api.Resources.OperationNotImplemented, operationName)); } var routeValues = new RouteValueDictionary() @@ -317,7 +317,7 @@ public Uri ResolveOperationDefinitionUrl(string operationName) routeName = RouteNames.SearchParameterStatusOperationDefinition; break; default: - throw new OperationNotImplementedException(string.Format(Resources.OperationNotImplemented, operationName)); + throw new OperationNotImplementedException(string.Format(Api.Resources.OperationNotImplemented, operationName)); } return GetRouteUri( diff --git a/src/Microsoft.Health.Fhir.Api/Features/SMART/SmartClinicalScopesMiddleware.cs b/src/Microsoft.Health.Fhir.Api/Features/SMART/SmartClinicalScopesMiddleware.cs index faf69685e8..4905eeaa98 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/SMART/SmartClinicalScopesMiddleware.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/SMART/SmartClinicalScopesMiddleware.cs @@ -152,14 +152,14 @@ public async Task Invoke( { if (authorizationConfiguration.ErrorOnMissingFhirUserClaim) { - throw new BadHttpRequestException(string.Format(Resources.FhirUserClaimMustBeURL, fhirUser)); + throw new BadHttpRequestException(string.Format(Api.Resources.FhirUserClaimMustBeURL, fhirUser)); } } catch (ArgumentNullException) { if (authorizationConfiguration.ErrorOnMissingFhirUserClaim) { - throw new BadHttpRequestException(Resources.FhirUserClaimCannotBeNull); + throw new BadHttpRequestException(Api.Resources.FhirUserClaimCannotBeNull); } } } diff --git a/src/Microsoft.Health.Fhir.Api/Features/Throttling/ThrottlingMiddleware.cs b/src/Microsoft.Health.Fhir.Api/Features/Throttling/ThrottlingMiddleware.cs index 947f39ff9d..dd4f4f4e80 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Throttling/ThrottlingMiddleware.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Throttling/ThrottlingMiddleware.cs @@ -40,7 +40,7 @@ public sealed class ThrottlingMiddleware : IAsyncDisposable, IDisposable // hard-coding these to minimize resource consumption when throttling private const string ThrottledContentType = "application/json; charset=utf-8"; - private static readonly ReadOnlyMemory _throttledBody = CreateThrottledBody(Resources.TooManyConcurrentRequests); + private static readonly ReadOnlyMemory _throttledBody = CreateThrottledBody(Api.Resources.TooManyConcurrentRequests); private readonly RequestDelegate _next; private readonly ILogger _logger; @@ -284,7 +284,7 @@ private async Task Return429(HttpContext context) { Interlocked.Increment(ref _currentPeriodRejectedCount); - _logger.LogWarning(Resources.TooManyConcurrentRequests + " Limit is {Limit}. Requests in flight {Requests}", _concurrentRequestLimit, _requestsInFlight); + _logger.LogWarning(Api.Resources.TooManyConcurrentRequests + " Limit is {Limit}. Requests in flight {Requests}", _concurrentRequestLimit, _requestsInFlight); context.Response.StatusCode = StatusCodes.Status429TooManyRequests; diff --git a/src/Microsoft.Health.Fhir.Core/Features/KnownHeaders.cs b/src/Microsoft.Health.Fhir.Core/Features/KnownHeaders.cs index e289a6012a..57bc665ab0 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/KnownHeaders.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/KnownHeaders.cs @@ -34,5 +34,7 @@ public static class KnownHeaders // #conditionalQueryParallelism - Header used to activate parallel conditional-query processing. public const string ConditionalQueryProcessingLogic = "x-conditionalquery-processing-logic"; + + public const string QueryCacheSetting = "x-ms-query-cache"; } } diff --git a/src/Microsoft.Health.Fhir.Core/Features/KnownQueryParameterNames.cs b/src/Microsoft.Health.Fhir.Core/Features/KnownQueryParameterNames.cs index dfb3ce2fb5..abf121dbac 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/KnownQueryParameterNames.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/KnownQueryParameterNames.cs @@ -61,10 +61,18 @@ public static class KnownQueryParameterNames public const string Container = "_container"; /// - /// Originally for CosmosDB workloads to hint that this request should run with a max parallel setting. + /// This setting is currently set by: + /// x-ms-query-latency-over-efficiency + /// x-conditionalquery-processing-logic + /// It is used to hint that the request should run with a max parallel setting. /// public const string OptimizeConcurrency = "_optimizeConcurrency"; + /// + /// This setting is controlled by the x-ms-query-cache-enabled header. It controls whether to use the query cache or not. + /// + public const string QueryCaching = "_queryCaching"; + /// /// The anonymization configuration /// diff --git a/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs index 51b29f5b9e..55d30e00d0 100644 --- a/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs +++ b/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs @@ -20,5 +20,7 @@ public class AzureApiForFhirRuntimeConfiguration : IFhirRuntimeConfiguration public bool IsTransactionSupported => false; public bool IsLatencyOverEfficiencySupported => true; + + public bool IsQueryCacheSupported => false; } } diff --git a/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs index 26ff55233a..e22302fb69 100644 --- a/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs +++ b/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs @@ -20,5 +20,7 @@ public class AzureHealthDataServicesRuntimeConfiguration : IFhirRuntimeConfigura public bool IsTransactionSupported => true; public bool IsLatencyOverEfficiencySupported => false; + + public bool IsQueryCacheSupported => true; } } diff --git a/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs index 94411f0c67..1160dd1ba9 100644 --- a/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs +++ b/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs @@ -33,5 +33,10 @@ public interface IFhirRuntimeConfiguration /// Supports the 'latency-over-efficiency' HTTP header. /// bool IsLatencyOverEfficiencySupported { get; } + + /// + /// Supports the query cache HTTP header. + /// + bool IsQueryCacheSupported { get; } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs index cee02b0442..c96b18e870 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs @@ -63,6 +63,7 @@ namespace Microsoft.Health.Fhir.Api.Controllers [ServiceFilter(typeof(OperationOutcomeExceptionFilterAttribute))] [ServiceFilter(typeof(ValidateFormatParametersAttribute))] [ServiceFilter(typeof(QueryLatencyOverEfficiencyFilterAttribute))] + [ServiceFilter(typeof(QueryCacheFilterAttribute))] [ValidateResourceTypeFilter] [ValidateModelState] public class FhirController : Controller 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 0acaed0a36..4184a19ce9 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 @@ -36,7 +36,6 @@ - @@ -44,7 +43,6 @@ - diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs b/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs index c79a4e4849..2c1610f15e 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs @@ -116,6 +116,7 @@ ResourceElement SetMetadata(Resource resource, string versionId, DateTimeOffset services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Support for resolve() FhirPathCompiler.DefaultSymbolTable.AddFhirExtensions(); diff --git a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/QueryCacheSetting.cs b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/QueryCacheSetting.cs new file mode 100644 index 0000000000..d5cbeafd79 --- /dev/null +++ b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/QueryCacheSetting.cs @@ -0,0 +1,15 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +namespace Microsoft.Health.Fhir.SqlServer.Features.Search +{ + internal class QueryCacheSetting + { + public const string Enabled = "enabled"; + public const string Disabled = "disabled"; + public const string Default = "default"; + public const string Both = "both"; + } +} diff --git a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs index 25d9eade9c..97b65c9c7b 100644 --- a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs +++ b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs @@ -144,7 +144,7 @@ public override async Task SearchAsync(SearchOptions searchOptions { SqlSearchOptions sqlSearchOptions = new SqlSearchOptions(searchOptions); - SearchResult searchResult = await SearchImpl(sqlSearchOptions, cancellationToken); + SearchResult searchResult = await RunSearch(sqlSearchOptions, cancellationToken); int resultCount = searchResult.Results.Count(r => r.SearchEntryMode == SearchEntryMode.Match); if (!sqlSearchOptions.IsSortWithFilter && @@ -179,7 +179,7 @@ public override async Task SearchAsync(SearchOptions searchOptions sqlSearchOptions.SortQuerySecondPhase = true; sqlSearchOptions.MaxItemCount -= resultCount; - searchResult = await SearchImpl(sqlSearchOptions, cancellationToken); + searchResult = await RunSearch(sqlSearchOptions, cancellationToken); finalResultsInOrder.AddRange(searchResult.Results); searchResult = new SearchResult( @@ -208,7 +208,7 @@ public override async Task SearchAsync(SearchOptions searchOptions sqlSearchOptions.CountOnly = true; // And perform a second read. - var countOnlySearchResult = await SearchImpl(sqlSearchOptions, cancellationToken); + var countOnlySearchResult = await RunSearch(sqlSearchOptions, cancellationToken); searchResult.TotalCount = countOnlySearchResult.TotalCount; } @@ -223,8 +223,53 @@ public override async Task SearchAsync(SearchOptions searchOptions return searchResult; } - private async Task SearchImpl(SqlSearchOptions sqlSearchOptions, CancellationToken cancellationToken) + private async Task RunSearch(SqlSearchOptions sqlSearchOptions, CancellationToken cancellationToken) { + var fhirContext = _requestContextAccessor.RequestContext; + if (fhirContext != null + && fhirContext.Properties.TryGetValue(KnownQueryParameterNames.QueryCaching, out object useQueryCacheObj) + && useQueryCacheObj != null) + { + var useQueryCache = Convert.ToString(useQueryCacheObj); + if (string.Equals(useQueryCache, QueryCacheSetting.Enabled, StringComparison.OrdinalIgnoreCase)) + { + return await SearchImpl(sqlSearchOptions, true, cancellationToken); + } + else if (string.Equals(useQueryCache, QueryCacheSetting.Disabled, StringComparison.OrdinalIgnoreCase)) + { + return await SearchImpl(sqlSearchOptions, false, cancellationToken); + } + else if (string.Equals(useQueryCache, QueryCacheSetting.Both, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogInformation("Running search with and without query cache."); + var stopwatch = Stopwatch.StartNew(); + + using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var token = tokenSource.Token; + + var tryWithQueryCache = SearchImpl(sqlSearchOptions, true, token); + var tryWithoutQueryCache = SearchImpl(sqlSearchOptions, false, token); + + var result = await Task.WhenAny(tryWithQueryCache, tryWithoutQueryCache); + await tokenSource.CancelAsync(); + + _logger.LogInformation("First search completed in {ElapsedMilliseconds}ms, query cache enabled: {QueryCacheEnabled}.", stopwatch.ElapsedMilliseconds, result == tryWithQueryCache); + return await result; + } + else // equals default or an invalid value + { + return await SearchImpl(sqlSearchOptions, _reuseQueryPlans.IsEnabled(_sqlRetryService), cancellationToken); + } + } + else + { + return await SearchImpl(sqlSearchOptions, _reuseQueryPlans.IsEnabled(_sqlRetryService), cancellationToken); + } + } + + private async Task SearchImpl(SqlSearchOptions sqlSearchOptions, bool reuseQueryPlans, CancellationToken cancellationToken) + { + Stopwatch stopwatch = Stopwatch.StartNew(); Expression searchExpression = sqlSearchOptions.Expression; // AND in the continuation token @@ -357,7 +402,7 @@ await _sqlRetryService.ExecuteSql( new HashingSqlQueryParameterManager(new SqlQueryParameterManager(sqlCommand.Parameters)), _model, _schemaInformation, - _reuseQueryPlans.IsEnabled(_sqlRetryService), + reuseQueryPlans, sqlException); expression.AcceptVisitor(queryGenerator, clonedSearchOptions); @@ -570,6 +615,7 @@ await _sqlRetryService.ExecuteSql( cancellationToken, true); // this enables reads from replicas + _logger.LogInformation("Search completed in {ElapsedMilliseconds}ms, query cache enabled: {QueryCacheEnabled}.", stopwatch.ElapsedMilliseconds, reuseQueryPlans); return searchResult; }