Skip to content
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
14 changes: 6 additions & 8 deletions Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -525,16 +525,14 @@ public System.Text.Json.JsonSerializerOptions UseSystemTextJsonSerializerWithOpt
/// Range.LengthAwareMinComparer/LengthAwareMaxComparer.
/// Setting the value to false will disable length-aware range comparator and switch to using the regular
/// Range.MinComparer/MaxComparer.
/// Can be controlled via the AZURE_COSMOS_USE_LENGTH_AWARE_RANGE_COMPARATOR environment variable.
/// </summary>
/// <value>
/// The default value is true.
/// </value>
internal bool UseLengthAwareRangeComparer { get; set; } =
#if !INTERNAL
true;
#else
false;
#endif
/// Defaults to true (false for INTERNAL builds). Reads from ConfigurationManager which
/// respects the AZURE_COSMOS_USE_LENGTH_AWARE_RANGE_COMPARATOR environment variable.
/// </value>
internal bool UseLengthAwareRangeComparer { get; set; } =
ConfigurationManager.IsLengthAwareRangeComparatorEnabled();

/// <summary>
/// (Direct/TCP) Controls the amount of idle time after which unused connections are closed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,8 @@ private async Task<CollectionRoutingMap> GetRoutingMapForCollectionAsync(
HashSet<string> goneRanges = new HashSet<string>(ranges.SelectMany(range => range.Parents ?? Enumerable.Empty<string>()));
routingMap = CollectionRoutingMap.TryCreateCompleteRoutingMap(
tuples.Where(tuple => !goneRanges.Contains(tuple.Item1.Id)),
string.Empty,
false,
string.Empty,
this.useLengthAwareRangeComparer,
changeFeedNextIfNoneMatch);
}
else
Expand Down
13 changes: 8 additions & 5 deletions Microsoft.Azure.Cosmos/src/Util/ConfigurationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -395,14 +395,17 @@ public static bool ForceBypassQueryParsing()

/// <summary>
/// Gets the boolean value indicating if length-aware range comparator is enabled.
/// Default: true for preview , false for GA.
/// Default: true for GA and Preview builds, false for INTERNAL builds.
/// Can be overridden via the AZURE_COSMOS_USE_LENGTH_AWARE_RANGE_COMPARATOR environment variable.
/// Setting the environment variable to false disables length-aware range comparator across all
/// usage sites (TryCombine, QueryRangeUtils, PartitionRoutingHelper).
/// </summary>
/// <returns>A boolean flag indicating if length-aware range comparator is enabled.</returns>
public static bool IsLengthAwareRangeComparatorEnabled()
{
bool defaultValue = false;
#if PREVIEW && !INTERNAL
defaultValue = true;
{
bool defaultValue = true;
#if INTERNAL
defaultValue = false;
#endif
return ConfigurationManager
.GetEnvironmentVariable(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//------------------------------------------------------------

namespace Microsoft.Azure.Cosmos.Tests
{
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Regression bar for PR #5866 — verifies that the
/// AZURE_COSMOS_USE_LENGTH_AWARE_RANGE_COMPARATOR environment variable
/// controls the LengthAware range comparator end-to-end:
///
/// ENV var
/// -> ConfigurationManager.IsLengthAwareRangeComparatorEnabled()
/// -> CosmosClientOptions.UseLengthAwareRangeComparer (property initializer)
/// -> ClientContextCore (useLengthAwareRangeComparer ctor arg)
/// -> DocumentClient.UseLengthAwareRangeComparer (consumed by
/// PartitionKeyRangeCache.TryCombine).
///
/// Before the fix, CosmosClientOptions.UseLengthAwareRangeComparer was a
/// hardcoded "true" literal gated only by the INTERNAL compile-time symbol,
/// so setting the env var to "false" had no effect on a real CosmosClient.
/// </summary>
[TestClass]
public class LengthAwareRangeComparerEnvVarTests
{
private const string EnvVarName = "AZURE_COSMOS_USE_LENGTH_AWARE_RANGE_COMPARATOR";
private const string AccountEndpoint = "https://localhost:8081/";

// Default the runtime-build expects when the env var is unset.
// Mirror the product code: true in all non-INTERNAL builds, false in INTERNAL.
#if INTERNAL
private const bool DefaultWhenUnset = false;
#else
private const bool DefaultWhenUnset = true;
#endif

private string priorEnvVarValue;

[TestInitialize]
public void TestInitialize()
{
this.priorEnvVarValue = Environment.GetEnvironmentVariable(EnvVarName);
Environment.SetEnvironmentVariable(EnvVarName, null);
}

[TestCleanup]
public void TestCleanup()
{
Environment.SetEnvironmentVariable(EnvVarName, this.priorEnvVarValue);
}

[DataTestMethod]
[DataRow(null, DefaultWhenUnset, DisplayName = "unset -> build default")]
[DataRow("false", false, DisplayName = "false -> false")]
[DataRow("true", true, DisplayName = "true -> true")]
[Owner("amudumba")]
public void IsLengthAwareRangeComparatorEnabled_RespectsEnvVar(string envValue, bool expected)
{
Environment.SetEnvironmentVariable(EnvVarName, envValue);

Assert.AreEqual(
expected,
ConfigurationManager.IsLengthAwareRangeComparatorEnabled(),
$"env={envValue ?? "<unset>"} did not produce expected={expected}.");
}

[DataTestMethod]
[DataRow(null, DefaultWhenUnset, DisplayName = "unset -> build default")]
[DataRow("false", false, DisplayName = "false -> false (regression for PR #5866)")]
[DataRow("true", true, DisplayName = "true -> true")]
[Owner("amudumba")]
public void CosmosClientOptions_DefaultUseLengthAwareRangeComparer_RespectsEnvVar(string envValue, bool expected)
{
Environment.SetEnvironmentVariable(EnvVarName, envValue);

CosmosClientOptions options = new CosmosClientOptions();

Assert.AreEqual(
expected,
options.UseLengthAwareRangeComparer,
$"env={envValue ?? "<unset>"} did not propagate to CosmosClientOptions.UseLengthAwareRangeComparer.");
}

[DataTestMethod]
// env-var-only rows (no explicit option on CosmosClientOptions):
[DataRow(null, null, DefaultWhenUnset, DisplayName = "unset, no explicit -> build default")]
[DataRow("false", null, false, DisplayName = "false, no explicit -> false (regression for PR #5866)")]
[DataRow("true", null, true, DisplayName = "true, no explicit -> true")]
// explicit-option-wins row: env says off, caller explicitly opts back in:
[DataRow("false", true, true, DisplayName = "false, explicit=true -> true (explicit option wins)")]
[Owner("amudumba")]
public void CosmosClient_UseLengthAwareRangeComparer_FlowsToDocumentClient(
string envValue,
bool? explicitOption,
bool expected)
{
Environment.SetEnvironmentVariable(EnvVarName, envValue);

CosmosClientOptions options = null;
if (explicitOption.HasValue)
{
options = new CosmosClientOptions
{
UseLengthAwareRangeComparer = explicitOption.Value,
};
}

using CosmosClient cosmosClient = new CosmosClient(
AccountEndpoint,
MockCosmosUtil.RandomInvalidCorrectlyFormatedAuthKey,
options);

Assert.AreEqual(
expected,
cosmosClient.ClientOptions.UseLengthAwareRangeComparer,
$"env={envValue ?? "<unset>"}, explicit={explicitOption?.ToString() ?? "<none>"} did not reach CosmosClient.ClientOptions on the real construction path.");

Assert.AreEqual(
expected,
cosmosClient.DocumentClient.UseLengthAwareRangeComparer,
$"env={envValue ?? "<unset>"}, explicit={explicitOption?.ToString() ?? "<none>"} did not flow through ClientContextCore into DocumentClient — this is the value PartitionKeyRangeCache.TryCombine consumes.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Microsoft.Azure.Cosmos.Tests
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -258,5 +259,110 @@ public async Task TryGetOverlappingRangesAsync_WhenGatewayThrowsServiceUnavailab
}
}
}

/// <summary>
/// Regression bar for PR #5866 (cold-start callsite).
///
/// <see cref="PartitionKeyRangeCache.GetRoutingMapForCollectionAsync"/> has two
/// sister callsites that materialize a <see cref="CollectionRoutingMap"/>:
/// * <c>previousRoutingMap == null</c> (cold-start) calls <c>TryCreateCompleteRoutingMap(...)</c>.
/// * <c>previousRoutingMap != null</c> (refresh) calls <c>previousRoutingMap.TryCombine(...)</c>.
///
/// PR #5551 wired the refresh callsite to <c>this.useLengthAwareRangeComparer</c>
/// but left the cold-start callsite with a literal <c>false</c>, so the first
/// routing map for every container in every <see cref="CosmosClient"/> was always
/// built with the regular <c>Range&lt;string&gt;.MinComparer/MaxComparer</c>
/// regardless of the env var or <see cref="CosmosClientOptions.UseLengthAwareRangeComparer"/>.
/// This test pins the cold-start callsite to consume the configured value.
/// </summary>
[DataTestMethod]
[DataRow(true, "LengthAwareMinComparer", "LengthAwareMaxComparer", DisplayName = "useLengthAwareRangeComparer=true bakes LengthAware comparers into cold-start CollectionRoutingMap")]
[DataRow(false, "MinComparer", "MaxComparer", DisplayName = "useLengthAwareRangeComparer=false bakes regular comparers into cold-start CollectionRoutingMap")]
[Owner("amudumba")]
public async Task GetRoutingMapForCollectionAsync_ColdStart_PropagatesUseLengthAwareRangeComparerToCollectionRoutingMap(
bool useLengthAwareRangeComparer,
string expectedMinComparerTypeName,
string expectedMaxComparerTypeName)
{
string eTag = "483";
string authToken = "token!";
string containerRId = "kjhsAA==";
string singlePkCollectionCache = "{\"_rid\":\"3FIlAOzjvyg=\",\"PartitionKeyRanges\":[{\"_rid\":\"3FIlAOzjvygCAAAAAAAAUA==\",\"id\":\"0\",\"_etag\":\"\\\"00005565-0000-0800-0000-621fd98a0000\\\"\",\"minInclusive\":\"\",\"maxExclusive\":\"FF\",\"ridPrefix\":0,\"_self\":\"dbs/3FIlAA==/colls/3FIlAOzjvyg=/pkranges/3FIlAOzjvygCAAAAAAAAUA==/\",\"throughputFraction\":1,\"status\":\"splitting\",\"parents\":[],\"_ts\":1646254474,\"_lsn\":44}],\"_count\":1}";
byte[] singlePkCollectionCacheByte = Encoding.UTF8.GetBytes(singlePkCollectionCache);

using (ITrace trace = Trace.GetRootTrace(this.TestContext.TestName, TraceComponent.Unknown, TraceLevel.Info))
{
Mock<IStoreModel> mockStoreModel = new();
Mock<CollectionCache> mockCollectionCache = new(false);
Mock<ICosmosAuthorizationTokenProvider> mockTokenProvider = new();
NameValueCollectionWrapper headers = new()
{
[HttpConstants.HttpHeaders.ETag] = eTag,
};

Mock<IDocumentClientInternal> mockDocumentClient = new Mock<IDocumentClientInternal>();
mockDocumentClient.Setup(client => client.ServiceEndpoint).Returns(new Uri("https://foo"));

using GlobalEndpointManager endpointManager = new(mockDocumentClient.Object, new ConnectionPolicy());

mockStoreModel.SetupSequence(x => x.ProcessMessageAsync(It.IsAny<DocumentServiceRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new DocumentServiceResponse(new MemoryStream(singlePkCollectionCacheByte),
new StoreResponseNameValueCollection()
{
ETag = eTag,
},
HttpStatusCode.OK))
.ReturnsAsync(new DocumentServiceResponse(null, headers, HttpStatusCode.NotModified, null));

mockTokenProvider.Setup(x => x.GetUserAuthorizationTokenAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<INameValueCollection>(), It.IsAny<AuthorizationTokenType>(), It.IsAny<ITrace>()))
.Returns(new ValueTask<string>(authToken));

// Act 1: drive a cold-start lookup (previousRoutingMap == null branch in GetRoutingMapForCollectionAsync).
PartitionKeyRangeCache partitionKeyRangeCache = new(
mockTokenProvider.Object,
mockStoreModel.Object,
mockCollectionCache.Object,
endpointManager,
useLengthAwareRangeComparer: useLengthAwareRangeComparer,
enableAsyncCacheExceptionNoSharing: false);

IReadOnlyList<PartitionKeyRange> ranges = await partitionKeyRangeCache.TryGetOverlappingRangesAsync(
containerRId,
FeedRangeEpk.FullRange.Range,
trace,
forceRefresh: false);
Assert.IsNotNull(ranges, "Cold-start lookup should return overlapping ranges.");

// Act 2: re-read the cached map (cache hit, previousValue: null) so we can inspect the baked-in comparers.
CollectionRoutingMap cachedMap = await partitionKeyRangeCache.TryLookupAsync(
collectionRid: containerRId,
previousValue: null,
request: null,
trace: trace);
Assert.IsNotNull(cachedMap, "Cached CollectionRoutingMap should exist after cold-start lookup.");

// Assert: the (MinComparer, MaxComparer) tuple baked into the CollectionRoutingMap
// ctor reflects the useLengthAwareRangeComparer wired into PartitionKeyRangeCache.
FieldInfo comparersField = typeof(CollectionRoutingMap).GetField(
"comparers",
BindingFlags.NonPublic | BindingFlags.Instance);
Assert.IsNotNull(comparersField, "CollectionRoutingMap.comparers private field must exist.");

object comparers = comparersField.GetValue(cachedMap);
Type tupleType = comparers.GetType();

object minComparer = tupleType.GetField("Item1").GetValue(comparers);
object maxComparer = tupleType.GetField("Item2").GetValue(comparers);

Assert.AreEqual(
expectedMinComparerTypeName,
minComparer.GetType().Name,
$"Cold-start routing map should bake {expectedMinComparerTypeName} when PartitionKeyRangeCache.useLengthAwareRangeComparer={useLengthAwareRangeComparer}.");
Assert.AreEqual(
expectedMaxComparerTypeName,
maxComparer.GetType().Name,
$"Cold-start routing map should bake {expectedMaxComparerTypeName} when PartitionKeyRangeCache.useLengthAwareRangeComparer={useLengthAwareRangeComparer}.");
}
}
}
}
Loading