Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System.Text;
using System.Threading.Tasks;
using Microsoft.Health.Fhir.Core.Features.Search.Hackathon;
using Microsoft.Health.Fhir.Tests.Common;
using Microsoft.Health.Test.Utilities;
using Xunit;

namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Search.Hackathon
{
[Trait(Traits.OwningTeam, OwningTeam.Fhir)]
[Trait(Traits.Category, Categories.Search)]
public sealed class QueryPlanSelectorTests
{
[Fact]
public void GivenExecutionTimes_WhereSettingsTRUEHasABetterPerformance_ThenSelectorShouldPreferTrue()
{
// This test simulates the case then SQL Query Plan Caching is beneficial.

QueryPlanSelector selector = new QueryPlanSelector();

string hash = "hash1";

// The following function simulates the execution time for each setting.
// For even iterations (setting=true), the execution time is 50ms.
// That will result in a faster execution, that will be learned by the selector.
// And the selector will eventually settle on setting=true.
var getValue = (int index) => index < 6 && index % 2 == 0 ? 100 : 50;

for (int i = 0; i < 20; i++)
{
bool setting = selector.GetQueryPlanCachingSetting(hash);

selector.ReportExecutionTime(hash, setting, getValue(i));
}

Assert.True(selector.GetQueryPlanCachingSetting(hash), "Based on the logic of this test, the selector should have settled on setting=true.");
}

[Fact]
public void GivenExecutionTimes_WhereSettingsFALSEHasABetterPerformance_ThenSelectorShouldPreferTrue()
{
// This test simulates the case then SQL Query Plan Caching is not beneficial.

QueryPlanSelector selector = new QueryPlanSelector();

string hash = "hash1";

// The following function simulates the execution time for each setting.
// For even iterations (setting=false), the execution time is 50ms.
// That will result in a faster execution, that will be learned by the selector.
// And the selector will eventually settle on setting=false.
var getValue = (int index) => index > 6 || index % 2 == 0 ? 50 : 100;

for (int i = 0; i < 20; i++)
{
bool setting = selector.GetQueryPlanCachingSetting(hash);

selector.ReportExecutionTime(hash, setting, getValue(i));
}

Assert.False(selector.GetQueryPlanCachingSetting(hash), "Based on the logic of this test, the selector should have settled on setting=false.");
}

[Fact]
public void GivenMultipleSequentialRequests_WithMultipleHashes_ThenTheSelectorShouldHandleIt()
{
QueryPlanSelector selector = new QueryPlanSelector();

string hash1 = "hash1";
string hash2 = "hash2";
string hash3 = "hash3";
string hash4 = "hash4";

for (int i = 0; i < 10; i++)
{
bool setting1 = selector.GetQueryPlanCachingSetting(hash1);
bool setting2 = selector.GetQueryPlanCachingSetting(hash2);
bool setting3 = selector.GetQueryPlanCachingSetting(hash3);
bool setting4 = selector.GetQueryPlanCachingSetting(hash4);

selector.ReportExecutionTime(hash1, setting1, setting1 ? 50 : 100);
selector.ReportExecutionTime(hash2, setting2, setting2 ? 100 : 50);
selector.ReportExecutionTime(hash3, setting3, setting3 ? 110 : 120);
selector.ReportExecutionTime(hash4, setting4, setting4 ? 30 : 20);
}

Assert.True(selector.GetQueryPlanCachingSetting(hash1));
Assert.False(selector.GetQueryPlanCachingSetting(hash2));
Assert.True(selector.GetQueryPlanCachingSetting(hash3));
Assert.False(selector.GetQueryPlanCachingSetting(hash4));
}

[Fact]
public async Task GivenMultipleParallelRequests_WithTwoDifferentHashes_ThenTheSelectorShouldHandleIt()
{
QueryPlanSelector selector = new QueryPlanSelector();

string hash1 = "hash1";
string hash2 = "hash2";
string hash3 = "hash3";
string hash4 = "hash4";

StringBuilder log = new StringBuilder();

var result = Parallel.For(0, 100, (i) =>
{
bool setting1 = selector.GetQueryPlanCachingSetting(hash1);
selector.ReportExecutionTime(hash1, setting1, setting1 ? 50 : 100);
log.AppendLine($"Hash1: Iteration {i}, Setting={setting1}");

bool setting2 = selector.GetQueryPlanCachingSetting(hash2);
selector.ReportExecutionTime(hash2, setting2, setting2 ? 100 : 50);

bool setting3 = selector.GetQueryPlanCachingSetting(hash3);
selector.ReportExecutionTime(hash3, setting3, setting3 ? 110 : 120);

bool setting4 = selector.GetQueryPlanCachingSetting(hash4);
selector.ReportExecutionTime(hash4, setting4, setting4 ? 30 : 20);
});

while (!result.IsCompleted)
{
await Task.Delay(10);
}

Assert.True(selector.GetQueryPlanCachingSetting(hash1));
Assert.False(selector.GetQueryPlanCachingSetting(hash2));
Assert.True(selector.GetQueryPlanCachingSetting(hash3));
Assert.False(selector.GetQueryPlanCachingSetting(hash4));
}

[Fact]
public void GivenSeveralCacheEntries_WhenReachedTheLimit_ThenTheDefaultValueShouldBeReturned()
{
const int maxNumberOfEntries = 500;

QueryPlanSelector selector = new QueryPlanSelector();

string hash = string.Empty;
bool setting = false;

for (int i = 0; i < maxNumberOfEntries; i++)
{
hash = $"hash{i}";

// Learning phase.
setting = selector.GetQueryPlanCachingSetting(hash);
Assert.False(setting);
selector.ReportExecutionTime(hash, setting, 1);

setting = selector.GetQueryPlanCachingSetting(hash);
Assert.True(setting);
selector.ReportExecutionTime(hash, setting, 1);
}

// After limit is reached, new entries are not allowed.
hash = $"hash{maxNumberOfEntries + 1}";
setting = selector.GetQueryPlanCachingSetting(hash);
Assert.False(setting);
setting = selector.GetQueryPlanCachingSetting(hash);
Assert.False(setting);
setting = selector.GetQueryPlanCachingSetting(hash);
Assert.False(setting);
setting = selector.GetQueryPlanCachingSetting(hash);
Assert.False(setting);

// After limit is reached, new entries are not allowed.
hash = $"hash{maxNumberOfEntries + 2}";
setting = selector.GetQueryPlanCachingSetting(hash);
Assert.False(setting);
setting = selector.GetQueryPlanCachingSetting(hash);
Assert.False(setting);
setting = selector.GetQueryPlanCachingSetting(hash);
Assert.False(setting);
setting = selector.GetQueryPlanCachingSetting(hash);
Assert.False(setting);

// While older entries are still allowed.
for (int i = 0; i < maxNumberOfEntries; i++)
{
hash = $"hash{i}";

// Learning phase.
setting = selector.GetQueryPlanCachingSetting(hash);
Assert.False(setting);
selector.ReportExecutionTime(hash, setting, 1);

setting = selector.GetQueryPlanCachingSetting(hash);
Assert.True(setting);
selector.ReportExecutionTime(hash, setting, 1);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,7 @@ public class OperationsConfiguration
public BulkDeleteJobConfiguration BulkDelete { get; set; } = new BulkDeleteJobConfiguration();

public BulkUpdateJobConfiguration BulkUpdate { get; set; } = new BulkUpdateJobConfiguration();

public QueryConfiguration Query { get; set; } = new QueryConfiguration();
}
}
15 changes: 15 additions & 0 deletions src/Microsoft.Health.Fhir.Core/Configs/QueryConfiguration.cs
Original file line number Diff line number Diff line change
@@ -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.Core.Configs
{
public class QueryConfiguration
{
/// <summary>
/// If Dynamic Sql Query Plan Selection is enabled.
/// </summary>
public bool DynamicSqlQueryPlanSelectionEnabled { get; set; } = false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ string ValueToString()
return $"(Field{BinaryOperator} {(ComponentIndex == null ? null : $"[{ComponentIndex}].")}{FieldName} {ValueToString()})";
}

public override string GetUniqueExpressionIdentifier()
{
return $"(Field{BinaryOperator} {(ComponentIndex == null ? null : $"[{ComponentIndex}].")}{FieldName})";
}

public override void AddValueInsensitiveHashCode(ref HashCode hashCode)
{
hashCode.Add(typeof(BinaryExpression));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ public override string ToString()
return $"({(Reversed ? "Reverse " : string.Empty)}Chain {ReferenceSearchParameter.Code}:{string.Join(", ", TargetResourceTypes)} {Expression})";
}

public override string GetUniqueExpressionIdentifier()
{
return $"({(Reversed ? "Reverse " : string.Empty)}Chain {ReferenceSearchParameter.Code}:{string.Join(", ", TargetResourceTypes)} {Expression.GetUniqueExpressionIdentifier()})";
}

public override void AddValueInsensitiveHashCode(ref HashCode hashCode)
{
hashCode.Add(typeof(ChainedExpression));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public override string ToString()
return $"(Compartment {CompartmentType} '{CompartmentId}')";
}

public override string GetUniqueExpressionIdentifier()
{
return $"(Compartment {CompartmentType})";
}

public override void AddValueInsensitiveHashCode(ref HashCode hashCode)
{
hashCode.Add(typeof(CompartmentSearchExpression));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@ public static NotReferencedExpression NotReferenced()
/// <inheritdoc />
public abstract override string ToString();

/// <summary>
/// Given an expression, returns its unique identifier, ignoring parameterizable values.
/// </summary>
public abstract string GetUniqueExpressionIdentifier();

/// <summary>
/// Accumulates a "value-insensitive" hash code of this instance, meaning it ignores parameterizable values.
/// For example, date=2013&amp;name=Smith and date=2014&amp;name=Trudeau would have the same hash code.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ public override string ToString()
return $"({(ComponentIndex == null ? null : $"[{ComponentIndex}].")}{FieldName} IN ({string.Join(", ", Values)}))";
}

public override string GetUniqueExpressionIdentifier()
{
return $"({(ComponentIndex == null ? null : $"[{ComponentIndex}].")}{FieldName})";
}

public override void AddValueInsensitiveHashCode(ref HashCode hashCode)
{
hashCode.Add(typeof(InExpression<T>));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ public override string ToString()
return $"({reversed}Include{iterate}{wildcard}{paramName}{targetType})";
}

public override string GetUniqueExpressionIdentifier()
{
return ToString();
}

private IReadOnlyCollection<string> GetRequiredResources()
{
if (Reversed)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public override string ToString()
return $"(MissingField {(ComponentIndex == null ? null : $"[{ComponentIndex}].")}{FieldName})";
}

public override string GetUniqueExpressionIdentifier()
{
return ToString();
}

public override void AddValueInsensitiveHashCode(ref HashCode hashCode)
{
hashCode.Add(typeof(MissingFieldException));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public override string ToString()
return $"({(!IsMissing ? "Not" : null)}MissingParam {Parameter.Name})";
}

public override string GetUniqueExpressionIdentifier()
{
return ToString();
}

public override void AddValueInsensitiveHashCode(ref HashCode hashCode)
{
hashCode.Add(typeof(MissingSearchParameterExpression));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ public override string ToString()
return $"({MultiaryOperation} {string.Join(' ', Expressions)})";
}

public override string GetUniqueExpressionIdentifier()
{
return $"({MultiaryOperation} {string.Join(' ', Expressions.Select(e => e.GetUniqueExpressionIdentifier()))})";
}

public override void AddValueInsensitiveHashCode(ref HashCode hashCode)
{
hashCode.Add(typeof(MultiaryExpression));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public override string ToString()
return $"(Not {Expression})";
}

public override string GetUniqueExpressionIdentifier()
{
return $"(Not {Expression.GetUniqueExpressionIdentifier()})";
}

public override void AddValueInsensitiveHashCode(ref HashCode hashCode)
{
hashCode.Add(typeof(NotExpression));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
// -------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using EnsureThat;

namespace Microsoft.Health.Fhir.Core.Features.Search.Expressions
Expand All @@ -30,6 +26,11 @@ public override string ToString()
return "NotReferenced";
}

public override string GetUniqueExpressionIdentifier()
{
return ToString();
}

public override void AddValueInsensitiveHashCode(ref HashCode hashCode)
{
hashCode.Add(typeof(NotReferencedExpression));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@
return $"(Param {Parameter.Code} {Expression})";
}

public override string GetUniqueExpressionIdentifier()
{
// ResourceType is a special case where the expression requires the value to be included.
if (Parameter.Code == SearchParameterNames.ResourceType)
{
return $"(Param {Parameter.Code} {Expression})";
}
else
{
return $"(Param {Parameter.Code} {Expression.GetUniqueExpressionIdentifier()})";
}
}

public override void AddValueInsensitiveHashCode(ref HashCode hashCode)
{
hashCode.Add(typeof(SearchParameterExpression));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public override string ToString()
return $"(Sort Param: {Parameter.Code})";
}

public override string GetUniqueExpressionIdentifier()
{
return ToString();
}

public override void AddValueInsensitiveHashCode(ref HashCode hashCode)
{
hashCode.Add(typeof(SortExpression));
Expand Down
Loading
Loading