Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new SQL hash calculation #4737

Merged
merged 4 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;
using System.Linq;
using System.Security.Cryptography;
using Microsoft.Health.Core.Extensions;
using Microsoft.Health.Fhir.Core.Features.Search;
using Microsoft.Health.Fhir.Core.Models;

namespace Microsoft.Health.Fhir.SqlServer.Features.Search
{
Expand Down Expand Up @@ -39,5 +44,30 @@ public SqlSearchOptions(SearchOptions searchOptions)
/// Performs a shallow clone of this instance
/// </summary>
public SqlSearchOptions CloneSqlSearchOptions() => (SqlSearchOptions)MemberwiseClone();

/// <summary>
/// Hashes the search option to indicate if two search options will return the same results.
/// UnsupportedSearchParams isn't inlcuded in the has because it isn't used in the actual search
/// </summary>
/// <returns>A hash of the search options</returns>
public string GetHash()
{
var expressionHash = default(HashCode);
Expression?.AddValueInsensitiveHashCode(ref expressionHash);

var sort = Sort?.Aggregate(string.Empty, (string result, (SearchParameterInfo param, SortOrder order) input) =>
{
return result + $"{input.param.Url}_{input.order}_";
});

var queryHints = QueryHints?.Aggregate(string.Empty, (string result, (string param, string value) input) =>
{
return result + $"{input.param}_{input.value}_";
});

var hashString = $"{ContinuationToken}_{FeedRange}_{CountOnly}_{IgnoreSearchParamHash}_{IncludeTotal}_{MaxItemCount}_{MaxItemCountSpecifiedByClient}_{IncludeCount}_{ResourceVersionTypes}_{OnlyIds}_{IsLargeAsyncOperation}_{SortQuerySecondPhase}_{IsSortWithFilter}_{DidWeSearchForSortValue}_{SortHasMissingModifier}_{expressionHash.ToHashCode()}_{sort}_{queryHints}";

return hashString.ComputeHash();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ internal class SqlServerSearchService : SearchService
private readonly RequestContextAccessor<IFhirRequestContext> _requestContextAccessor;
private const int _defaultNumberOfColumnsReadFromResult = 11;
private readonly SearchParameterInfo _fakeLastUpdate = new SearchParameterInfo(SearchParameterNames.LastUpdated, SearchParameterNames.LastUpdated);
private readonly ISqlQueryHashCalculator _queryHashCalculator;
private readonly IParameterStore _parameterStore;
private static ResourceSearchParamStats _resourceSearchParamStats;
private static object _locker = new object();
Expand All @@ -92,7 +91,6 @@ public SqlServerSearchService(
SchemaInformation schemaInformation,
RequestContextAccessor<IFhirRequestContext> requestContextAccessor,
ICompressedRawResourceConverter compressedRawResourceConverter,
ISqlQueryHashCalculator queryHashCalculator,
IParameterStore parameterStore,
ILogger<SqlServerSearchService> logger)
: base(searchOptionsFactory, fhirDataStore, logger)
Expand All @@ -117,7 +115,6 @@ public SqlServerSearchService(
_smartCompartmentSearchRewriter = smartCompartmentSearchRewriter;
_chainFlatteningRewriter = chainFlatteningRewriter;
_sqlRetryService = sqlRetryService;
_queryHashCalculator = queryHashCalculator;
_parameterStore = parameterStore;
_logger = logger;

Expand Down Expand Up @@ -364,7 +361,7 @@ await _sqlRetryService.ExecuteSql(
SqlCommandSimplifier.RemoveRedundantParameters(stringBuilder, sqlCommand.Parameters, _logger);

var queryText = stringBuilder.ToString();
var queryHash = _queryHashCalculator.CalculateHash(queryText);
var queryHash = clonedSearchOptions.GetHash();
_logger.LogInformation("SQL Search Service query hash: {QueryHash}", queryHash);
var customQuery = CustomQueries.CheckQueryHash(connection, queryHash, _logger);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,6 @@ public static IFhirServerBuilder AddSqlServer(this IFhirServerBuilder fhirServer
.AsSelf()
.AsImplementedInterfaces();

services.Add<SqlQueryHashCalculator>()
.Singleton()
.AsImplementedInterfaces();

services.Add<SqlServerSearchService>()
.Scoped()
.AsSelf()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// -------------------------------------------------------------------------------------------------
// 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 System.Linq;
using Microsoft.Extensions.Logging;

namespace Microsoft.Health.Fhir.Tests.Integration
{
public class ReadableLogger<T> : ILogger<T>
{
private List<string> _logs = new List<string>();

Check notice

Code scanning / CodeQL

Missed 'readonly' opportunity Note test

Field '_logs' can be 'readonly'.

public IDisposable BeginScope<TState>(TState state)
{
throw new NotImplementedException();
}

public bool IsEnabled(LogLevel logLevel)
{
throw new NotImplementedException();
}

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
_logs.Add(formatter(state, exception));
}

public void LogError(string message)
{
_logs.Add(message);
}

public void LogInformation(string message)
{
_logs.Add(message);
}

public void LogWarning(string message)
{
_logs.Add(message);
}

public bool TryGetLatestLog(string content, out string match)
{
if (_logs.Any(l => l.Contains(content)))
{
match = _logs.FindLast(l => l.Contains(content));
return true;
}

match = null;
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Features\Operations\Reindex\ReindexJobTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Features\Operations\Reindex\ReindexSearchTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Features\Smart\SmartSearchTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\ReadableLogger.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Persistence\SqlAzurePipelinesWorkloadIdentityAuthenticationProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Persistence\SqlComplexQueryTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Persistence\FhirStorageVersioningPolicyTests.cs" />
Expand All @@ -49,6 +50,5 @@
<Compile Include="$(MSBuildThisFileDirectory)Persistence\SqlServerSqlCompatibilityTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Persistence\QueueClientTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Persistence\SqlServerTransactionScopeTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Persistence\TestSqlHashCalculator.cs" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
using Microsoft.Health.Fhir.Core.Models;
using Microsoft.Health.Fhir.Core.UnitTests;
using Microsoft.Health.Fhir.Core.UnitTests.Extensions;
using Microsoft.Health.Fhir.SqlServer.Features.Search;
using Microsoft.Health.Fhir.Tests.Common;
using Microsoft.Health.Fhir.Tests.Common.FixtureParameters;
using Microsoft.Health.Fhir.Tests.Common.Mocks;
Expand Down Expand Up @@ -125,12 +126,12 @@ internal FhirStorageTestsFixture(IServiceProvider fixture)

public RequestContextAccessor<IFhirRequestContext> FhirRequestContextAccessor => _fixture.GetRequiredService<RequestContextAccessor<IFhirRequestContext>>();

public TestSqlHashCalculator SqlQueryHashCalculator => _fixture.GetRequiredService<TestSqlHashCalculator>();

public GetResourceHandler GetResourceHandler { get; set; }

public IQueueClient QueueClient => _fixture.GetRequiredService<IQueueClient>();

internal ReadableLogger<SqlServerSearchService> ReadableLogger => _fixture.GetRequiredService<ReadableLogger<SqlServerSearchService>>();

public void Dispose()
{
(_fixture as IDisposable)?.Dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@ public async Task GivenASqlQuery_IfAStoredProcExistsWithMatchingHash_ThenStoredP
// Query before adding an sproc to the database
await _fixture.SearchService.SearchAsync(KnownResourceTypes.Patient, query, CancellationToken.None);

var hash = _fixture.SqlQueryHashCalculator.MostRecentSqlHash;
var hash = _fixture.ReadableLogger.TryGetLatestLog("SQL Search Service query hash:", out var hashValue) ? hashValue.Substring(31) : null;
Assert.NotNull(hash);

// assert an sproc was not used
Assert.False(await CheckIfSprocUsed(hash));
Expand All @@ -199,11 +200,16 @@ public async Task GivenASqlQuery_IfAStoredProcExistsWithMatchingHash_ThenStoredP
// Query after adding an sproc to the database
var sw = Stopwatch.StartNew();
var sprocWasUsed = false;

// Change parameter values to test that hash is independent of parameter values
query = new[] { Tuple.Create("birthdate", "gt1900-01-01"), Tuple.Create("birthdate", "lt2000-01-01"), Tuple.Create("address-city", "Town"), Tuple.Create("address-state", "State") };

while (sw.Elapsed.TotalSeconds < 100) // previous single try after 1.1 sec delay was not reliable.
{
await Task.Delay(300);
await _fixture.SearchService.SearchAsync(KnownResourceTypes.Patient, query, CancellationToken.None);
Assert.Equal(hash, _fixture.SqlQueryHashCalculator.MostRecentSqlHash);
var newHash = _fixture.ReadableLogger.TryGetLatestLog("SQL Search Service query hash:", out var newHashValue) ? newHashValue.Substring(31) : null;
Assert.Equal(hash, newHash);
if (await CheckIfSprocUsed(hash))
{
sprocWasUsed = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public class SqlServerFhirStorageTestsFixture : IServiceProvider, IAsyncLifetime
private SupportedSearchParameterDefinitionManager _supportedSearchParameterDefinitionManager;
private SearchParameterStatusManager _searchParameterStatusManager;
private SqlQueueClient _sqlQueueClient;
private ReadableLogger<SqlServerSearchService> _readableLogger;

public SqlServerFhirStorageTestsFixture()
: this(SchemaVersionConstants.Max, GetDatabaseName())
Expand Down Expand Up @@ -125,8 +126,6 @@ internal SqlServerFhirStorageTestsFixture(int maximumSupportedSchemaVersion, str

internal SchemaInformation SchemaInformation { get; private set; }

internal ISqlQueryHashCalculator SqlQueryHashCalculator { get; private set; }

internal static string GetDatabaseName(string test = null)
{
return $"{ModelInfoProvider.Version}{(test == null ? string.Empty : $"_{test}")}_{DateTimeOffset.UtcNow.ToString("s").Replace("-", string.Empty).Replace(":", string.Empty)}_{Guid.NewGuid().ToString().Replace("-", string.Empty)}";
Expand Down Expand Up @@ -278,7 +277,7 @@ public async Task InitializeAsync()
var compartmentSearchRewriter = new CompartmentSearchRewriter(new Lazy<ICompartmentDefinitionManager>(() => compartmentDefinitionManager), new Lazy<ISearchParameterDefinitionManager>(() => _searchParameterDefinitionManager));
var smartCompartmentSearchRewriter = new SmartCompartmentSearchRewriter(compartmentSearchRewriter, new Lazy<ISearchParameterDefinitionManager>(() => _searchParameterDefinitionManager));

SqlQueryHashCalculator = new TestSqlHashCalculator();
_readableLogger = new ReadableLogger<SqlServerSearchService>();

_searchService = new SqlServerSearchService(
searchOptionsFactory,
Expand All @@ -295,9 +294,8 @@ public async Task InitializeAsync()
SchemaInformation,
_fhirRequestContextAccessor,
new CompressedRawResourceConverter(),
SqlQueryHashCalculator,
new SqlServerParameterStore(SqlConnectionBuilder, NullLogger<SqlServerParameterStore>.Instance),
NullLogger<SqlServerSearchService>.Instance);
_readableLogger);

ISearchParameterSupportResolver searchParameterSupportResolver = Substitute.For<ISearchParameterSupportResolver>();
searchParameterSupportResolver.IsSearchParameterSupported(Arg.Any<SearchParameterInfo>()).Returns((true, false));
Expand Down Expand Up @@ -413,9 +411,9 @@ object IServiceProvider.GetService(Type serviceType)
return _sqlQueueClient;
}

if (serviceType == typeof(TestSqlHashCalculator))
if (serviceType == typeof(ReadableLogger<SqlServerSearchService>))
{
return SqlQueryHashCalculator as TestSqlHashCalculator;
return _readableLogger;
}

return null;
Expand Down

This file was deleted.