From 25291de8cb28622d223921f7bb7fb92491dcac07 Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Thu, 21 Aug 2025 20:37:12 +0200 Subject: [PATCH] .NET: chore: support retries on Cosmos storage creation (#402) * support retries * tests + registration options * fix ordering .. * HK + update packages * fix paths * Update dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/CosmosTestFixture.cs * re create project and fix some pk usage * fix all tests * try workflow? * wip 1 * fix definition * try with cosmos_use_emulator env? * try ignore SSL errors? * other cert verifications * hardcode to 8081? * proper valuation of ENV * logging * ensure db exsists for CI * bump * cleanup * fix usage * nit comment * try only release for stability? * try skip some flaky tests * merge fixes + rollback container * reimplement with iasyncdisposable pattern * remove example doc struct --- .../dotnet-cosmosdb-integration-tests.yml | 158 ++++++++++++++++++ dotnet/Directory.Packages.props | 17 +- dotnet/agent-framework-dotnet.slnx | 5 + .../AgentWebChat.AppHost.csproj | 2 +- .../ActorDocuments.cs | 23 +-- .../CosmosActorStateStorage.cs | 47 ++++-- .../LazyCosmosContainer.cs | 106 ++++++++++-- ....AI.Agents.Runtime.Storage.CosmosDB.csproj | 1 + .../AppHost.cs | 12 +- .../CosmosDB.Testing.AppHost.csproj} | 11 +- .../CosmosDBTestConstants.cs | 24 +++ .../Properties/launchSettings.json | 1 + .../appsettings.Development.json | 8 + .../CosmosDB.Testing.AppHost/appsettings.json | 9 + .../CosmosDBTestConstants.cs | 18 -- ...CosmosActorStateStorageConcurrencyTests.cs | 10 +- .../CosmosActorStateStorageListKeysTests.cs | 12 +- .../CosmosActorStateStorageTests.cs | 18 +- .../CosmosTestFixture.cs | 28 +++- .../LazyCosmosContainerTests.cs | 23 +-- ...ents.Runtime.Storage.CosmosDB.Tests.csproj | 2 +- .../SkipOnEmulatorFactAttribute.cs | 11 +- 22 files changed, 436 insertions(+), 110 deletions(-) create mode 100644 .github/workflows/dotnet-cosmosdb-integration-tests.yml rename dotnet/tests/CosmosDB.IntegrationTests/{Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost => CosmosDB.Testing.AppHost}/AppHost.cs (53%) rename dotnet/tests/CosmosDB.IntegrationTests/{Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost.csproj => CosmosDB.Testing.AppHost/CosmosDB.Testing.AppHost.csproj} (59%) create mode 100644 dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/CosmosDBTestConstants.cs rename dotnet/tests/CosmosDB.IntegrationTests/{Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost => CosmosDB.Testing.AppHost}/Properties/launchSettings.json (95%) create mode 100644 dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/appsettings.Development.json create mode 100644 dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/appsettings.json delete mode 100644 dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost/CosmosDBTestConstants.cs diff --git a/.github/workflows/dotnet-cosmosdb-integration-tests.yml b/.github/workflows/dotnet-cosmosdb-integration-tests.yml new file mode 100644 index 0000000000..c947097762 --- /dev/null +++ b/.github/workflows/dotnet-cosmosdb-integration-tests.yml @@ -0,0 +1,158 @@ +# +# This workflow runs Cosmos DB integration tests using the Cosmos DB emulator. +# + +name: dotnet-cosmosdb-integration-tests + +on: + workflow_dispatch: + pull_request: + branches: ["main", "feature*"] + paths: + - dotnet/tests/CosmosDB.IntegrationTests/** + - dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/** + - '.github/workflows/dotnet-cosmosdb-integration-tests.yml' + merge_group: + branches: ["main"] + push: + branches: ["main", "feature*"] + paths: + - dotnet/tests/CosmosDB.IntegrationTests/** + - dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/** + - '.github/workflows/dotnet-cosmosdb-integration-tests.yml' + schedule: + - cron: "0 2 * * *" # Run at 2 AM UTC daily + +env: + COSMOSDB_TESTS_USE_EMULATOR_CICD: "true" + +jobs: + build-and-test: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + include: + - { targetFramework: "net9.0", os: "ubuntu-latest", configuration: Release } + # - { targetFramework: "net9.0", os: "ubuntu-latest", configuration: Debug } + + services: + cosmosdb: + image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest + ports: + - 8081:8081 + env: + AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE: "false" + AZURE_COSMOS_EMULATOR_PARTITION_COUNT: "20" # the more the better for stable tests + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + sparse-checkout: | + . + .github + dotnet + - name: Setup dotnet + uses: actions/setup-dotnet@v4.3.1 + with: + global-json-file: ${{ github.workspace }}/dotnet/global.json + - name: Build dotnet solutions + shell: bash + run: | + export SOLUTIONS=$(find ./dotnet/ -type f -name "*.slnx" | tr '\n' ' ') + for solution in $SOLUTIONS; do + dotnet build $solution -c ${{ matrix.configuration }} --warnaserror + done + - name: Package install check + shell: bash + # All frameworks are only built for the release configuration, so we only run this step for the release configuration + # and dotnet new doesn't support net472 + if: matrix.configuration == 'Release' && matrix.targetFramework != 'net472' + run: | + TEMP_DIR=$(mktemp -d) + + export SOLUTIONS=$(find ./dotnet/ -type f -name "*.slnx" | tr '\n' ' ') + for solution in $SOLUTIONS; do + dotnet pack $solution /property:TargetFrameworks=${{ matrix.targetFramework }} -c ${{ matrix.configuration }} --no-build --no-restore --output "$TEMP_DIR/artifacts" + done + + pushd "$TEMP_DIR" + + # Create a new console app to test the package installation + dotnet new console -f ${{ matrix.targetFramework }} --name packcheck --output consoleapp + + # Create minimal nuget.config and use only dotnet nuget commands + echo '' > consoleapp/nuget.config + + # Add sources with local first using dotnet nuget commands + dotnet nuget add source ../artifacts --name local --configfile consoleapp/nuget.config + dotnet nuget add source https://api.nuget.org/v3/index.json --name nuget.org --configfile consoleapp/nuget.config + + # Change to project directory to ensure local nuget.config is used + pushd consoleapp + dotnet add packcheck.csproj package Microsoft.Extensions.AI.Agents --prerelease + dotnet build -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} packcheck.csproj + + # Clean up + popd + popd + rm -rf "$TEMP_DIR" + + - name: Wait for Cosmos DB Emulator to be ready + run: | + set -e + for i in $(seq 1 120); do + if curl -sk https://localhost:8081/_explorer/emulator.pem -o /dev/null; then + echo "Emulator is up." + break + fi + echo "Waiting for emulator... ($i/120)" + sleep 2 + done + + - name: Install emulator TLS certificate into system trust store + run: | + set -e + sudo apt-get update + sudo apt-get install -y ca-certificates curl openssl + # Fetch the PEM directly from the emulator's explorer endpoint + curl -sk https://localhost:8081/_explorer/emulator.pem -o cosmos-emulator.crt + # Install with the correct .crt extension so update-ca-certificates picks it up + sudo cp cosmos-emulator.crt /usr/local/share/ca-certificates/cosmos-emulator.crt + sudo update-ca-certificates + + - name: Verify TLS now trusts the emulator + run: | + # Use -servername to avoid SNI warning and check verification + echo | openssl s_client -connect localhost:8081 -servername localhost 2>/dev/null | grep -E "Verify return code|subject=|issuer=" + # Expect: "Verify return code: 0 (ok)" + + - name: Run Cosmos DB Integration Tests + shell: bash + run: | + # Run the specific CosmosDB integration tests + dotnet test ./dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.csproj \ + -f ${{ matrix.targetFramework }} \ + -c ${{ matrix.configuration }} \ + --no-build \ + -v Normal \ + --logger trx \ + --collect:"XPlat Code Coverage" \ + --results-directory:"TestResults/Coverage/" \ + -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.ExcludeByAttribute=GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute + + # Generate test reports and check coverage + - name: Generate test reports + uses: danielpalme/ReportGenerator-GitHub-Action@5.4.11 + with: + reports: "./TestResults/Coverage/**/coverage.cobertura.xml" + targetdir: "./TestResults/Reports" + reporttypes: "HtmlInline;JsonSummary" + + - name: Upload coverage report artifact + uses: actions/upload-artifact@v4 + with: + name: CosmosDB-CoverageReport-${{ matrix.os }}-${{ matrix.targetFramework }}-${{ matrix.configuration }} + path: ./TestResults/Reports \ No newline at end of file diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index b05d4c5fd3..a90c7abc6f 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -5,14 +5,18 @@ true true + + + 9.4.1 + - - - - - - + + + + + + @@ -61,6 +65,7 @@ + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 33156ad0f9..59cd9f847c 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -28,6 +28,7 @@ + @@ -121,6 +122,10 @@ + + + + diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AppHost/AgentWebChat.AppHost.csproj b/dotnet/samples/AgentWebChat/AgentWebChat.AppHost/AgentWebChat.AppHost.csproj index f200bd03b1..c79dcd67d3 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AppHost/AgentWebChat.AppHost.csproj +++ b/dotnet/samples/AgentWebChat/AgentWebChat.AppHost/AgentWebChat.AppHost.csproj @@ -1,6 +1,6 @@  - + Exe diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/ActorDocuments.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/ActorDocuments.cs index 4b8b3cb296..c7fe2f2b45 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/ActorDocuments.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/ActorDocuments.cs @@ -11,13 +11,6 @@ namespace Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB; /// the entire actor's state for optimistic concurrency control. /// This document contains no actor state data. It only serves to track last modified /// time and provide a single ETag for the actor's state. -/// -/// Example structure: -/// { -/// "id": "rootdoc", // Root document ID (constant per actor partition) -/// "actorId": "actor-123", // Partition key (actor ID) -/// "lastModified": "2024-...", // Timestamp -/// } /// public sealed class ActorRootDocument { @@ -27,9 +20,13 @@ public sealed class ActorRootDocument public string Id { get; set; } = default!; /// - /// The actor ID. + /// The actor type. /// - public string ActorId { get; set; } = default!; + public string ActorType { get; set; } = default!; + /// + /// The actor key. + /// + public string ActorKey { get; set; } = default!; /// /// The last modified timestamp. @@ -55,9 +52,13 @@ public sealed class ActorStateDocument public string Id { get; set; } = default!; /// - /// The actor ID. + /// The actor type. /// - public string ActorId { get; set; } = default!; + public string ActorType { get; set; } = default!; + /// + /// The actor key. + /// + public string ActorKey { get; set; } = default!; /// /// The logical key for the state entry. diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/CosmosActorStateStorage.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/CosmosActorStateStorage.cs index 4bdf383b85..e96254a993 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/CosmosActorStateStorage.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/CosmosActorStateStorage.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB; /// /// Cosmos DB implementation of actor state storage. /// -public class CosmosActorStateStorage : IActorStateStorage +public class CosmosActorStateStorage : IActorStateStorage, IAsyncDisposable { private readonly LazyCosmosContainer _lazyContainer; private const string InitialEtag = "0"; // Initial ETag value when no state exists @@ -47,8 +47,8 @@ public class CosmosActorStateStorage : IActorStateStorage } var container = await this._lazyContainer.GetContainerAsync().ConfigureAwait(false); - var batch = container.CreateTransactionalBatch(GetPartitionKey(actorId)); - var actorIdStr = actorId.ToString(); + var (partitionKey, actorType, actorKey) = BuildPartitionKey(actorId); + var batch = container.CreateTransactionalBatch(partitionKey); // Add data operations to batch foreach (var op in operations) @@ -61,7 +61,8 @@ public class CosmosActorStateStorage : IActorStateStorage var item = new ActorStateDocument { Id = docId, - ActorId = actorIdStr, + ActorType = actorType, + ActorKey = actorKey, Key = set.Key, Value = set.Value }; @@ -83,7 +84,8 @@ public class CosmosActorStateStorage : IActorStateStorage var newRoot = new ActorRootDocument { Id = RootDocumentId, - ActorId = actorId.ToString(), + ActorType = actorType, + ActorKey = actorKey, LastModified = DateTimeOffset.UtcNow, }; @@ -103,6 +105,7 @@ public class CosmosActorStateStorage : IActorStateStorage var result = await batch.ExecuteAsync(cancellationToken).ConfigureAwait(false); if (!result.IsSuccessStatusCode) { + _ = result.ErrorMessage; return new WriteResponse(eTag: string.Empty, success: false); } @@ -135,6 +138,8 @@ public class CosmosActorStateStorage : IActorStateStorage // Read root document first to get actor-level ETag string actorETag = await this.GetActorETagAsync(container, actorId, cancellationToken).ConfigureAwait(false); + var actorType = actorId.Type.ToString(); + var actorKey = actorId.Key; foreach (var op in operations) { @@ -162,14 +167,16 @@ public class CosmosActorStateStorage : IActorStateStorage QueryDefinition query; if (!string.IsNullOrEmpty(list.KeyPrefix)) { - query = new QueryDefinition("SELECT c.key FROM c WHERE c.actorId = @actorId AND c.key != null AND STARTSWITH(c.key, @keyPrefix)") - .WithParameter("@actorId", actorId.ToString()) + query = new QueryDefinition("SELECT c.key FROM c WHERE c.actorType = @actorType AND c.actorKey = @actorKey AND c.key != null AND STARTSWITH(c.key, @keyPrefix)") + .WithParameter("@actorType", actorType) + .WithParameter("@actorKey", actorKey) .WithParameter("@keyPrefix", list.KeyPrefix); } else { - query = new QueryDefinition("SELECT c.key FROM c WHERE c.actorId = @actorId AND c.key != null") - .WithParameter("@actorId", actorId.ToString()); + query = new QueryDefinition("SELECT c.key FROM c WHERE c.actorType = @actorType AND c.actorKey = @actorKey AND c.key != null") + .WithParameter("@actorType", actorType) + .WithParameter("@actorKey", actorKey); } var requestOptions = new QueryRequestOptions @@ -212,7 +219,18 @@ public class CosmosActorStateStorage : IActorStateStorage private const string RootDocumentId = "rootdoc"; private static PartitionKey GetPartitionKey(ActorId actorId) - => new(actorId.ToString()); + { + var (partitionKey, _, _) = BuildPartitionKey(actorId); + return partitionKey; + } + + private static (PartitionKey partitionKey, string actorType, string actorKey) BuildPartitionKey(ActorId actorId) + { + var actorType = actorId.Type.ToString(); + var actorKey = actorId.Key; + var partitionKey = new PartitionKeyBuilder().Add(actorType).Add(actorKey).Build(); + return (partitionKey, actorType, actorKey); + } /// /// Gets the current ETag for the actor's root document. @@ -234,4 +252,13 @@ public class CosmosActorStateStorage : IActorStateStorage return InitialEtag; } } + + /// + /// Disposes the Cosmos DB container asynchronously. + /// + public async ValueTask DisposeAsync() + { + await this._lazyContainer.DisposeAsync().ConfigureAwait(false); + GC.SuppressFinalize(this); + } } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/LazyCosmosContainer.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/LazyCosmosContainer.cs index 1695786636..af4be62a81 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/LazyCosmosContainer.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/LazyCosmosContainer.cs @@ -1,26 +1,30 @@ // Copyright (c) Microsoft. All rights reserved. - using System; using System.Collections.ObjectModel; +using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Cosmos; - namespace Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB; -#pragma warning disable VSTHRD011 // Use AsyncLazy - /// /// A lazy wrapper around a Cosmos DB Container. /// This avoids performing async I/O-bound operations (i.e. Cosmos DB setup) during /// DI registration, deferring them until first access. /// -internal sealed class LazyCosmosContainer +internal sealed class LazyCosmosContainer : IAsyncDisposable { + private readonly static Random s_random = new(); + private readonly CosmosClient? _cosmosClient; private readonly string? _databaseName; private readonly string? _containerName; - private readonly Lazy> _lazyContainer; + + private readonly CancellationTokenSource _cts = new(); + private Task? _initTask; + + // internal for testing + internal readonly static string[] CosmosPartitionKeyPaths = ["/actorType", "/actorKey"]; /// /// LazyCosmosContainer constructor that initializes the container lazily. @@ -30,9 +34,7 @@ internal sealed class LazyCosmosContainer this._cosmosClient = cosmosClient ?? throw new ArgumentNullException(nameof(cosmosClient)); this._databaseName = databaseName ?? throw new ArgumentNullException(nameof(databaseName)); this._containerName = containerName ?? throw new ArgumentNullException(nameof(containerName)); - this._lazyContainer = new Lazy>(this.InitializeContainerAsync, LazyThreadSafetyMode.ExecutionAndPublication); } - /// /// LazyCosmosContainer constructor that accepts an existing Container instance. /// @@ -43,20 +45,59 @@ internal sealed class LazyCosmosContainer throw new ArgumentNullException(nameof(container)); } - this._lazyContainer = new Lazy>(() => Task.FromResult(container), LazyThreadSafetyMode.ExecutionAndPublication); + this._initTask = Task.FromResult(container); } /// /// Gets the Container, initializing it if necessary. /// - public Task GetContainerAsync() => this._lazyContainer.Value; + public Task GetContainerAsync() + => this._initTask ??= this.InitializeWithRetryAsync(this._cts.Token); - private async Task InitializeContainerAsync() + private async Task InitializeWithRetryAsync(CancellationToken cancellationToken) + { + var baseDelay = TimeSpan.FromSeconds(1); + var maxDelay = TimeSpan.FromSeconds(30); + var previousDelay = baseDelay; + + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + return await this.InitializeContainerAsync(cancellationToken).ConfigureAwait(false); + } + catch (CosmosException ex) when (IsTransient(ex)) + { + // If server provided RetryAfter, respect it but add a small jitter so clients don't retry in perfect sync. + if (ex.RetryAfter is not null && ex.RetryAfter > TimeSpan.Zero) + { + var retry = ex.RetryAfter.Value; + var jitterMs = this.RandomNextDouble() * retry.TotalMilliseconds; // 0..retry + var delay = retry + TimeSpan.FromMilliseconds(jitterMs); + await Task.Delay(delay, cancellationToken).ConfigureAwait(false); + previousDelay = delay; + continue; + } + + // sleep = min(maxDelay, random(baseDelay, previousDelay * 3)) + var minMs = baseDelay.TotalMilliseconds; + var maxMs = Math.Min(maxDelay.TotalMilliseconds, Math.Max(minMs, previousDelay.TotalMilliseconds * 3)); + var sleepMs = this.RandomNextDouble() * (maxMs - minMs) + minMs; + var jitterDelay = TimeSpan.FromMilliseconds(sleepMs); + + await Task.Delay(jitterDelay, cancellationToken).ConfigureAwait(false); + previousDelay = jitterDelay; + } + } + } + + private async Task InitializeContainerAsync(CancellationToken cancellationToken) { // Create database if it doesn't exist - var database = await this._cosmosClient!.CreateDatabaseIfNotExistsAsync(this._databaseName!).ConfigureAwait(false); + var database = await this._cosmosClient!.CreateDatabaseIfNotExistsAsync(this._databaseName!, cancellationToken: cancellationToken).ConfigureAwait(false); - var containerProperties = new ContainerProperties(this._containerName!, "/actorId") + var containerProperties = new ContainerProperties(this._containerName!, CosmosPartitionKeyPaths) { Id = this._containerName!, IndexingPolicy = new IndexingPolicy @@ -64,17 +105,50 @@ internal sealed class LazyCosmosContainer IndexingMode = IndexingMode.Consistent, Automatic = true }, - PartitionKeyPaths = ["/actorId"] + PartitionKeyPaths = CosmosPartitionKeyPaths }; // Add composite index for efficient queries containerProperties.IndexingPolicy.CompositeIndexes.Add(new Collection { - new() { Path = "/actorId", Order = CompositePathSortOrder.Ascending }, + new() { Path = "/actorType", Order = CompositePathSortOrder.Ascending }, + new() { Path = "/actorKey", Order = CompositePathSortOrder.Ascending }, new() { Path = "/key", Order = CompositePathSortOrder.Ascending } }); - var container = await database.Database.CreateContainerIfNotExistsAsync(containerProperties).ConfigureAwait(false); + var container = await database.Database.CreateContainerIfNotExistsAsync(containerProperties, cancellationToken: cancellationToken).ConfigureAwait(false); return container.Container; } + + private static bool IsTransient(Exception exception) + { + return exception switch + { + CosmosException cosmosEx => cosmosEx.StatusCode switch + { +#if NET9_0_OR_GREATER + HttpStatusCode.TooManyRequests => true, // 429 - Rate limited +#endif + HttpStatusCode.InternalServerError => true, // 500 - Server error + HttpStatusCode.BadGateway => true, // 502 - Bad gateway + HttpStatusCode.ServiceUnavailable => true, // 503 - Service unavailable + HttpStatusCode.GatewayTimeout => true, // 504 - Gateway timeout + HttpStatusCode.RequestTimeout => true, // 408 - Request timeout + _ => false + }, + TaskCanceledException or OperationCanceledException or ArgumentException => false, + _ => true // Retry other exceptions (network issues, etc.) + }; + } + +#pragma warning disable CA5394 // Do not use insecure randomness + private double RandomNextDouble() => s_random.NextDouble(); +#pragma warning restore CA5394 // Do not use insecure randomness + + public ValueTask DisposeAsync() + { + this._cts?.Cancel(); + this._cts?.Dispose(); + return default; + } } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.csproj b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.csproj index 4b451d2d35..b452277d3b 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.csproj +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.csproj @@ -9,6 +9,7 @@ + diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost/AppHost.cs b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/AppHost.cs similarity index 53% rename from dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost/AppHost.cs rename to dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/AppHost.cs index 61090b7f5e..6f5a47c6c2 100644 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost/AppHost.cs +++ b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/AppHost.cs @@ -1,10 +1,18 @@ // Copyright (c) Microsoft. All rights reserved. -using Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests; + +using CosmosDB.Testing.AppHost; var builder = DistributedApplication.CreateBuilder(args); var cosmosDb = builder.AddAzureCosmosDB(CosmosDBTestConstants.TestCosmosDbName); -if (CosmosDBTestConstants.UseEmulatorForTesting) +if (CosmosDBTestConstants.UseEmulatorInCICD) +{ + // Emulator created in the CI/CD pipeline gives more control over some settings and port-configuration today. + // It probably should be configured here to use 8081 port + setup the partition count and throughput, but it's not supported in Aspire yet, so leaving as a placeholder. + // Once Aspire's emulator is suported, the emulator in CI/CD can be removed. + cosmosDb.RunAsEmulator(emulator => emulator.WithLifetime(ContainerLifetime.Persistent)); +} +else if (CosmosDBTestConstants.UseAspireEmulatorForTesting) { cosmosDb.RunAsEmulator(emulator => emulator.WithLifetime(ContainerLifetime.Persistent)); } diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost.csproj b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/CosmosDB.Testing.AppHost.csproj similarity index 59% rename from dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost.csproj rename to dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/CosmosDB.Testing.AppHost.csproj index 8f7ef08ad8..feb032f7d8 100644 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost.csproj +++ b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/CosmosDB.Testing.AppHost.csproj @@ -1,16 +1,15 @@ - + - + - + Exe net9.0 enable enable - 80048040-aaf1-4f44-9970-8aef39651edf - true + cb8630a8-ec5e-4676-a2b0-4497965c809d false - + diff --git a/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/CosmosDBTestConstants.cs b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/CosmosDBTestConstants.cs new file mode 100644 index 0000000000..8b958ac80f --- /dev/null +++ b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/CosmosDBTestConstants.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. + +//using System.Linq.Expressions; + +namespace CosmosDB.Testing.AppHost; + +public static class CosmosDBTestConstants +{ + public const string TestCosmosDbName = "ActorStateStorageTests"; + public const string TestCosmosDbDatabaseName = "state-database"; + + //Set to use the CosmosDB emulator for testing via environment variable. + //Example: set COSMOSDB_TESTS_USE_EMULATOR = true in your environment. + //Warning: Using the emulator may cause test flakiness. + public static bool UseAspireEmulatorForTesting => string.Equals( + Environment.GetEnvironmentVariable("COSMOSDB_TESTS_USE_EMULATOR"), + "true", + StringComparison.OrdinalIgnoreCase); + + public static bool UseEmulatorInCICD => string.Equals( + Environment.GetEnvironmentVariable("COSMOSDB_TESTS_USE_EMULATOR_CICD"), + "true", + StringComparison.OrdinalIgnoreCase); +} diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost/Properties/launchSettings.json b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/Properties/launchSettings.json similarity index 95% rename from dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost/Properties/launchSettings.json rename to dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/Properties/launchSettings.json index 596bd53611..3b06925f1c 100644 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost/Properties/launchSettings.json +++ b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/Properties/launchSettings.json @@ -21,6 +21,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19080", "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20201" } diff --git a/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/appsettings.Development.json b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/appsettings.json b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/appsettings.json new file mode 100644 index 0000000000..31c092aa45 --- /dev/null +++ b/dotnet/tests/CosmosDB.IntegrationTests/CosmosDB.Testing.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost/CosmosDBTestConstants.cs b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost/CosmosDBTestConstants.cs deleted file mode 100644 index 43707f208b..0000000000 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.AppHost/CosmosDBTestConstants.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests; - -public static class CosmosDBTestConstants -{ - public const string TestCosmosDbName = "ActorStateStorageTests"; - public const string TestCosmosDbDatabaseName = "state-database"; - - // Set to use the CosmosDB emulator for testing via environment variable. - // Example: set COSMOSDB_TESTS_USE_EMULATOR=true in your environment. - // Warning: Using the emulator may cause test flakiness. - public static bool UseEmulatorForTesting => - string.Equals( - Environment.GetEnvironmentVariable("COSMOSDB_TESTS_USE_EMULATOR"), - "true", - StringComparison.OrdinalIgnoreCase); -} diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageConcurrencyTests.cs b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageConcurrencyTests.cs index 52b64dafc5..03edf21f97 100644 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageConcurrencyTests.cs +++ b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageConcurrencyTests.cs @@ -31,7 +31,7 @@ public class CosmosActorStateStorageConcurrencyTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); var key = "testKey"; @@ -70,7 +70,7 @@ public class CosmosActorStateStorageConcurrencyTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); // Setup initial state @@ -185,7 +185,7 @@ public class CosmosActorStateStorageConcurrencyTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); var key = "testKey"; @@ -258,7 +258,7 @@ public class CosmosActorStateStorageConcurrencyTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); // Fresh actor var key = "testKey"; @@ -308,7 +308,7 @@ public class CosmosActorStateStorageConcurrencyTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); // Non-existent actor var key = "testKey"; diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageListKeysTests.cs b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageListKeysTests.cs index 87d3c2fbb9..22f0dcd7c6 100644 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageListKeysTests.cs +++ b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageListKeysTests.cs @@ -26,7 +26,7 @@ public class CosmosActorStateStorageListKeysTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); var prefixKey1 = "prefix_key1"; @@ -69,7 +69,7 @@ public class CosmosActorStateStorageListKeysTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); var key1 = "key1"; @@ -107,7 +107,7 @@ public class CosmosActorStateStorageListKeysTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); // Act - List keys for actor with no state @@ -132,7 +132,7 @@ public class CosmosActorStateStorageListKeysTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); var key1 = "key1"; @@ -173,7 +173,7 @@ public class CosmosActorStateStorageListKeysTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); var key1 = "key1"; @@ -225,7 +225,7 @@ public class CosmosActorStateStorageListKeysTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); // Create keys with different prefixes diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageTests.cs b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageTests.cs index 14d6529b13..97ff06c026 100644 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageTests.cs +++ b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/CosmosActorStateStorageTests.cs @@ -26,7 +26,7 @@ public class CosmosActorStateStorageTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); var key = "testKey"; @@ -52,7 +52,7 @@ public class CosmosActorStateStorageTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); var key1 = "key1"; @@ -154,7 +154,7 @@ public class CosmosActorStateStorageTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); var key = "testKey"; @@ -196,7 +196,7 @@ public class CosmosActorStateStorageTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId1 = new ActorId("TestActor1", Guid.NewGuid().ToString()); var testActorId2 = new ActorId("TestActor2", Guid.NewGuid().ToString()); @@ -242,7 +242,7 @@ public class CosmosActorStateStorageTests // Arrange using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); var emptyOperations = new List(); // Act & Assert @@ -259,7 +259,7 @@ public class CosmosActorStateStorageTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); var readOperations = new List @@ -284,7 +284,7 @@ public class CosmosActorStateStorageTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); // Create a complex object with various types @@ -357,7 +357,7 @@ public class CosmosActorStateStorageTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); var key1 = "key1"; @@ -424,7 +424,7 @@ public class CosmosActorStateStorageTests using var cts = new CancellationTokenSource(s_defaultTimeout); var cancellationToken = cts.Token; - var storage = new CosmosActorStateStorage(this._fixture.Container); + await using var storage = new CosmosActorStateStorage(this._fixture.Container); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); // Test keys with special characters that need sanitization diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/CosmosTestFixture.cs b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/CosmosTestFixture.cs index c43ce25cbd..e9a6941cf5 100644 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/CosmosTestFixture.cs +++ b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/CosmosTestFixture.cs @@ -3,6 +3,7 @@ using System.Text.Json; using Aspire.Hosting; using Azure.Identity; +using CosmosDB.Testing.AppHost; using Microsoft.Azure.Cosmos; using Microsoft.Extensions.Logging; @@ -30,7 +31,7 @@ public class CosmosTestFixture : IAsyncLifetime var cancellationToken = cts.Token; var appHost = await DistributedApplicationTestingBuilder - .CreateAsync(cancellationToken); + .CreateAsync(cancellationToken); appHost.Services.AddLogging(logging => { @@ -47,7 +48,16 @@ public class CosmosTestFixture : IAsyncLifetime this.App = await appHost.BuildAsync(cancellationToken).WaitAsync(cancellationToken); await this.App.StartAsync(cancellationToken).WaitAsync(cancellationToken); - var cs = await this.App.GetConnectionStringAsync(CosmosDBTestConstants.TestCosmosDbName, cancellationToken); + var connectionString = await this.App.GetConnectionStringAsync(CosmosDBTestConstants.TestCosmosDbName, cancellationToken); + if (CosmosDBTestConstants.UseEmulatorInCICD) + { + // Emulator is setup in the CI/CD pipeline, so we will not use one produced by Aspire. + // For simplicity, we override the connection string here with the well-known emulator connection string. + // https://learn.microsoft.com/en-us/azure/cosmos-db/emulator + + connectionString = "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;"; + } + CosmosClientOptions ccoptions = new() { UseSystemTextJsonSerializerWithOptions = new JsonSerializerOptions() @@ -57,23 +67,29 @@ public class CosmosTestFixture : IAsyncLifetime } }; - if (CosmosDBTestConstants.UseEmulatorForTesting) + if (CosmosDBTestConstants.UseAspireEmulatorForTesting || CosmosDBTestConstants.UseEmulatorInCICD) { ccoptions.ConnectionMode = ConnectionMode.Gateway; ccoptions.LimitToEndpoint = true; - this.CosmosClient = new CosmosClient(cs, ccoptions); + this.CosmosClient = new CosmosClient(connectionString, ccoptions); } else { - this.CosmosClient = new CosmosClient(cs, new DefaultAzureCredential(), ccoptions); + this.CosmosClient = new CosmosClient(connectionString, new DefaultAzureCredential(), ccoptions); } var database = this.CosmosClient.GetDatabase(CosmosDBTestConstants.TestCosmosDbDatabaseName); + // raise throughput to avoid parallel test execution failures + var throughputProperties = ThroughputProperties.CreateAutoscaleThroughput(100000); + + // Ensure database exists. It will be a no-op if it was already created before. + _ = await this.CosmosClient.CreateDatabaseIfNotExistsAsync(CosmosDBTestConstants.TestCosmosDbDatabaseName, throughputProperties); + var containerProperties = new ContainerProperties() { Id = "CosmosActorStateStorageTests", - PartitionKeyPath = "/actorId" + PartitionKeyPaths = LazyCosmosContainer.CosmosPartitionKeyPaths }; this.Container = await database.CreateContainerIfNotExistsAsync(containerProperties); diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/LazyCosmosContainerTests.cs b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/LazyCosmosContainerTests.cs index 07925d2a60..2df7f01e3d 100644 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/LazyCosmosContainerTests.cs +++ b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/LazyCosmosContainerTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Text.Json; +using CosmosDB.Testing.AppHost; using Microsoft.Azure.Cosmos; namespace Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests; @@ -25,7 +26,7 @@ public class LazyCosmosContainerTests { // Arrange using var cts = new CancellationTokenSource(s_defaultTimeout); - var lazyContainer = new LazyCosmosContainer(this._fixture.Container); + await using var lazyContainer = new LazyCosmosContainer(this._fixture.Container); // Act var result = await lazyContainer.GetContainerAsync(); @@ -39,7 +40,7 @@ public class LazyCosmosContainerTests { // Arrange using var cts = new CancellationTokenSource(s_defaultTimeout); - var lazyContainer = new LazyCosmosContainer(this._fixture.Container); + await using var lazyContainer = new LazyCosmosContainer(this._fixture.Container); // Act var result1 = await lazyContainer.GetContainerAsync(); @@ -52,7 +53,7 @@ public class LazyCosmosContainerTests Assert.Same(this._fixture.Container, result1); } - [Fact] + [SkipOnEmulatorFact] public async Task GetContainerAsync_WithCosmosClient_ShouldInitializeAndWorkCorrectlyAsync() { // Arrange @@ -61,7 +62,7 @@ public class LazyCosmosContainerTests // Create a unique container name for this test var testContainerName = $"LazyContainerTest_{Guid.NewGuid():N}"; - var lazyContainer = new LazyCosmosContainer(this._fixture.CosmosClient, CosmosDBTestConstants.TestCosmosDbDatabaseName, testContainerName); + await using var lazyContainer = new LazyCosmosContainer(this._fixture.CosmosClient, CosmosDBTestConstants.TestCosmosDbDatabaseName, testContainerName); try { @@ -74,7 +75,7 @@ public class LazyCosmosContainerTests // Verify the container can perform basic operations var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); - var storage = new CosmosActorStateStorage(lazyContainer); + await using var storage = new CosmosActorStateStorage(lazyContainer); var key = "testKey"; var value = JsonSerializer.SerializeToElement("testValue"); @@ -111,7 +112,7 @@ public class LazyCosmosContainerTests // Create a unique container name for this test var testContainerName = $"LazyContainerTest_{Guid.NewGuid():N}"; - var lazyContainer = new LazyCosmosContainer(this._fixture.CosmosClient, CosmosDBTestConstants.TestCosmosDbDatabaseName, testContainerName); + await using var lazyContainer = new LazyCosmosContainer(this._fixture.CosmosClient, CosmosDBTestConstants.TestCosmosDbDatabaseName, testContainerName); try { @@ -148,7 +149,7 @@ public class LazyCosmosContainerTests // Create a unique container name for this test var testContainerName = $"LazyContainerTest_{Guid.NewGuid():N}"; - var lazyContainer = new LazyCosmosContainer(this._fixture.CosmosClient, CosmosDBTestConstants.TestCosmosDbDatabaseName, testContainerName); + await using var lazyContainer = new LazyCosmosContainer(this._fixture.CosmosClient, CosmosDBTestConstants.TestCosmosDbDatabaseName, testContainerName); try { @@ -211,7 +212,7 @@ public class LazyCosmosContainerTests Assert.Throws(() => new LazyCosmosContainer(this._fixture.CosmosClient, "test-db", null!)); } - [Fact] + [SkipOnEmulatorFact] public async Task LazyCosmosContainer_WithInternalConstructor_ShouldWorkWithCosmosActorStateStorageAsync() { // Arrange @@ -220,12 +221,12 @@ public class LazyCosmosContainerTests // Create a unique container name for this test var testContainerName = $"LazyContainerTest_{Guid.NewGuid():N}"; - var lazyContainer = new LazyCosmosContainer(this._fixture.CosmosClient, CosmosDBTestConstants.TestCosmosDbDatabaseName, testContainerName); + await using var lazyContainer = new LazyCosmosContainer(this._fixture.CosmosClient, CosmosDBTestConstants.TestCosmosDbDatabaseName, testContainerName); try { // Act - Create storage using the internal constructor (like DI would) - var storage = new CosmosActorStateStorage(lazyContainer); + await using var storage = new CosmosActorStateStorage(lazyContainer); var testActorId = new ActorId("TestActor", Guid.NewGuid().ToString()); var key = "testKey"; @@ -277,7 +278,7 @@ public class LazyCosmosContainerTests // Use an invalid database name that should cause Cosmos to reject it var invalidDatabaseName = new string('a', 256); // Database names have limits - var lazyContainer = new LazyCosmosContainer(this._fixture.CosmosClient, invalidDatabaseName, "test-container"); + await using var lazyContainer = new LazyCosmosContainer(this._fixture.CosmosClient, invalidDatabaseName, "test-container"); // Act & Assert await Assert.ThrowsAsync(async () => await lazyContainer.GetContainerAsync()); diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.csproj b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.csproj index 9f3271c68a..fcfeaf9462 100644 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.csproj +++ b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests.csproj @@ -13,8 +13,8 @@ - + diff --git a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/SkipOnEmulatorFactAttribute.cs b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/SkipOnEmulatorFactAttribute.cs index b1508873f6..fcd1feb8be 100644 --- a/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/SkipOnEmulatorFactAttribute.cs +++ b/dotnet/tests/CosmosDB.IntegrationTests/Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests/SkipOnEmulatorFactAttribute.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using CosmosDB.Testing.AppHost; + namespace Microsoft.Extensions.AI.Agents.Runtime.Storage.CosmosDB.Tests; /// @@ -13,9 +15,14 @@ public sealed class SkipOnEmulatorFactAttribute : FactAttribute /// public SkipOnEmulatorFactAttribute() { - if (CosmosDBTestConstants.UseEmulatorForTesting) + if (CosmosDBTestConstants.UseAspireEmulatorForTesting) { - this.Skip = "Skipping test on CosmosDB emulator."; + this.Skip = "Skipping test on Aspire-configured CosmosDB emulator."; + } + + if (CosmosDBTestConstants.UseEmulatorInCICD) + { + this.Skip = "Skipping test on CICD-configured CosmosDB emulator."; } } }