diff --git a/.github/workflows/k6-test-multi.yml b/.github/workflows/k6-test-multi.yml index ea3e439..f52f2de 100644 --- a/.github/workflows/k6-test-multi.yml +++ b/.github/workflows/k6-test-multi.yml @@ -3,7 +3,6 @@ name: k6 Test with Publish - Preview & Final permissions: contents: read actions: read - id-token: write on: push: @@ -15,7 +14,7 @@ on: version: description: "NuGet package version to use" required: false - default: "1.1.0-beta.1" + default: "10.1.0-beta.1" jobs: pack-nupkgs: @@ -86,15 +85,20 @@ jobs: with: dotnet-version: '9.0.x' - - name: NuGet login (OIDC → temp API key) + - name: Install .NET 10 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: NuGet login uses: NuGet/login@v1 - id: login + id: nuget-login with: user: ${{ secrets.NUGET_USER }} - name: Publish NuGet packages env: - NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY }} + NUGET_API_KEY: ${{ steps.nuget-login.outputs.NUGET_API_KEY }} run: | for pkg in ./nupkgs/*.nupkg; do echo "Publishing $pkg..." diff --git a/.github/workflows/k6-test-single.yml b/.github/workflows/k6-test-single.yml index d342e39..37d060d 100644 --- a/.github/workflows/k6-test-single.yml +++ b/.github/workflows/k6-test-single.yml @@ -51,12 +51,6 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - - name: Install Docker Compose - run: | - sudo curl -L "https://github.com/docker/compose/releases/download/v2.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose - sudo chmod +x /usr/local/bin/docker-compose - docker-compose version - - name: Download Environment run: | @@ -93,10 +87,10 @@ jobs: path: ${{ env.PROJECT_FOLDER }}/nupkgs - name: Build and start services - run: docker-compose -f test/integration/k6-environment/payment-processor/docker-compose.yml up -d + run: docker compose -f test/integration/k6-environment/payment-processor/docker-compose.yml up -d - name: Build and start services - run: cd $PROJECT_FOLDER && docker-compose up -d --build + run: cd $PROJECT_FOLDER && docker compose up -d --build - name: Wait for nginx to be healthy (5 successes) run: | @@ -138,24 +132,35 @@ jobs: retention-days: 1 - name: Create Comment MD File + if: always() run: | echo "## k6 Test Results" > comment.md echo "Hash: ${{ github.sha }}" >> comment.md - echo "\`\`\`json" >> comment.md - cat ${RESULTS_FILE} >> comment.md - echo "\n\`\`\`" >> comment.md + echo '```json' >> comment.md + + if [ -f "${RESULTS_FILE}" ]; then + cat "${RESULTS_FILE}" >> comment.md + else + echo "// No k6 results file found (${RESULTS_FILE})" >> comment.md + fi + + echo '```' >> comment.md - name: Append Docker Compose Logs + if: always() run: | echo "## Docker Compose Logs" >> comment.md echo '```log' >> comment.md - cd $PROJECT_FOLDER && docker-compose logs >> ../comment.md || true + cd $PROJECT_FOLDER && docker compose logs + cd $PROJECT_FOLDER && docker compose logs >> comment.md || true echo '```' >> comment.md - name: Echo comment.md contents + if: always() run: cat comment.md - name: Upload comment.md as artifact + if: always() uses: actions/upload-artifact@v4 with: name: comment-md-${{ env.MODE }}-${{ env.PROJECT }}-${{ matrix.run_id }} diff --git a/.github/workflows/pack-nupkgs.yml b/.github/workflows/pack-nupkgs.yml index 6665271..7cb169d 100644 --- a/.github/workflows/pack-nupkgs.yml +++ b/.github/workflows/pack-nupkgs.yml @@ -35,6 +35,11 @@ jobs: with: dotnet-version: '9.0.x' + - name: Install .NET 10 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - name: Determine version id: version run: | diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 4f0bf89..327b62c 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -33,6 +33,11 @@ jobs: with: dotnet-version: '9.0.x' + - name: Install .NET 10 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/src/ReactiveLock.Core/ReactiveLock.Core.csproj b/src/ReactiveLock.Core/ReactiveLock.Core.csproj index 75763b6..0b77ae1 100644 --- a/src/ReactiveLock.Core/ReactiveLock.Core.csproj +++ b/src/ReactiveLock.Core/ReactiveLock.Core.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0 + net8.0;net9.0;net10.0 enable enable diff --git a/src/ReactiveLock.DependencyInjection/ReactiveLock.DependencyInjection.csproj b/src/ReactiveLock.DependencyInjection/ReactiveLock.DependencyInjection.csproj index 2bd921d..80ff40b 100644 --- a/src/ReactiveLock.DependencyInjection/ReactiveLock.DependencyInjection.csproj +++ b/src/ReactiveLock.DependencyInjection/ReactiveLock.DependencyInjection.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0 + net8.0;net9.0;net10.0 enable enable $(TargetsForTfmSpecificBuildOutput);IncludeProjectReferencesWithPrivateAssetsAttributeInPackage diff --git a/src/ReactiveLock.Distributed.Grpc/ReactiveLock.Distributed.Grpc.csproj b/src/ReactiveLock.Distributed.Grpc/ReactiveLock.Distributed.Grpc.csproj index b2b17b3..5b312ce 100644 --- a/src/ReactiveLock.Distributed.Grpc/ReactiveLock.Distributed.Grpc.csproj +++ b/src/ReactiveLock.Distributed.Grpc/ReactiveLock.Distributed.Grpc.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0 + net8.0;net9.0;net10.0 enable enable $(TargetsForTfmSpecificBuildOutput);IncludeProjectReferencesWithPrivateAssetsAttributeInPackage diff --git a/src/ReactiveLock.Distributed.Grpc/ReactiveLockGrpcTrackerExtensions.cs b/src/ReactiveLock.Distributed.Grpc/ReactiveLockGrpcTrackerExtensions.cs index c7370d5..aaee7df 100644 --- a/src/ReactiveLock.Distributed.Grpc/ReactiveLockGrpcTrackerExtensions.cs +++ b/src/ReactiveLock.Distributed.Grpc/ReactiveLockGrpcTrackerExtensions.cs @@ -33,16 +33,14 @@ /// public static class ReactiveLockGrpcTrackerExtensions { - private static bool? IsInitializing { get; set; } - private static ConcurrentQueue RegisteredLocks { get; } = new(); - private static string? StoredInstanceName { get; set; } - private static List RemoteClients { get; set; } = new(); + private static ReactiveLockGrpcTrackerExtensionsState? ExtensionsState { get; set; } public static void InitializeDistributedGrpcReactiveLock(this IServiceCollection services, string instanceName, params string[] replicaGrpcServers) { - ReactiveLockConventions.RegisterFactory(services); - StoredInstanceName = instanceName; - RemoteClients.AddRange( + InitializeDistributedGrpcReactiveLock(services, instanceName); + ArgumentNullException.ThrowIfNull(replicaGrpcServers); + + ExtensionsState?.RemoteClients.AddRange( replicaGrpcServers.Select(url => new ReactiveLockGrpcClientAdapter( new ReactiveLockGrpcClient(GrpcChannel.ForAddress(url)) @@ -50,6 +48,27 @@ public static void InitializeDistributedGrpcReactiveLock(this IServiceCollection ) ); } + + public static void InitializeDistributedGrpcReactiveLock( + this IServiceCollection services, + string instanceName, + params IReactiveLockGrpcClientAdapter[] remoteClients) + { + InitializeDistributedGrpcReactiveLock(services, instanceName); + ArgumentNullException.ThrowIfNull(remoteClients); + + ExtensionsState?.RemoteClients.AddRange(remoteClients); + } + + private static void InitializeDistributedGrpcReactiveLock(IServiceCollection services, string instanceName) + { + ReactiveLockConventions.RegisterFactory(services); + + ExtensionsState = string.IsNullOrEmpty(instanceName) + ? null + : new(instanceName); + } + /// /// Registers distributed gRPC reactive lock services, configuring lock state, controller, and handlers. /// @@ -91,7 +110,7 @@ public static IServiceCollection AddDistributedGrpcReactiveLock( TimeSpan instanceExpirationPeriodTimeSpan, TimeSpan instanceRecoverPeriodTimeSpan) resiliencyParameters = default) { - if (RemoteClients.Count == 0 || string.IsNullOrEmpty(StoredInstanceName)) + if (ExtensionsState == null || ExtensionsState.RemoteClients.Count == 0) { throw new InvalidOperationException( "InstanceName not initialized. Call InitializeDistributedGrpcReactiveLock before adding distributed Grpc reactive locks."); @@ -100,9 +119,8 @@ public static IServiceCollection AddDistributedGrpcReactiveLock( ReactiveLockConventions.RegisterState(services, lockKey, onLockedHandlers, onUnlockedHandlers); ReactiveLockConventions.RegisterController(services, lockKey, _ => { - var isInitializing = IsInitializing.HasValue && IsInitializing.Value; - var isNotInitializing = !isInitializing; - var hasPendingLockRegistrations = !RegisteredLocks.IsEmpty; + var isNotInitializing = !ExtensionsState.IsInitializing; + var hasPendingLockRegistrations = !ExtensionsState.RegisteredLocks.IsEmpty; if (isNotInitializing && hasPendingLockRegistrations) { @@ -111,16 +129,16 @@ public static IServiceCollection AddDistributedGrpcReactiveLock( Please ensure you're calling 'await app.UseDistributedGrpcReactiveLockAsync();' on your IApplicationBuilder instance after 'var app = builder.Build();'."); } - var store = new ReactiveLockGrpcTrackerStore(RemoteClients, StoredInstanceName, customAsyncStorePolicy, + var store = new ReactiveLockGrpcTrackerStore(ExtensionsState.RemoteClients, ExtensionsState.InstanceName, customAsyncStorePolicy, resiliencyParameters, lockKey); return new ReactiveLockTrackerController(store, busyThreshold); }); - RegisteredLocks.Enqueue(lockKey); + ExtensionsState.RegisteredLocks.Enqueue(lockKey); return services; } - + private static async Task SubscribeToUpdates( IReactiveLockGrpcClientAdapter client, string storedInstanceName, @@ -157,16 +175,21 @@ public static async Task UseDistributedGrpcReactiveLockAsync( this IApplicationBuilder app, IAsyncPolicy? customAsyncSubscriberPolicy = default) { - IsInitializing = true; - var factory = app.ApplicationServices.GetRequiredService(); + if (ExtensionsState == null || ExtensionsState.RemoteClients.Count == 0) + { + throw new InvalidOperationException( + "InstanceName not initialized. Call InitializeDistributedGrpcReactiveLock before adding distributed Grpc reactive locks."); + } - var instanceStoredInstanceName = StoredInstanceName!; - var instanceRemoteClients = RemoteClients; + ExtensionsState.IsInitializing = true; + var factory = app.ApplicationServices.GetRequiredService(); + var instanceStoredInstanceName = ExtensionsState.InstanceName; + var instanceRemoteClients = ExtensionsState.RemoteClients; var readySignals = new List(); var retryPolicy = ReactiveLockPollyPolicies.UseOrCreateDefaultRetryPolicy(customAsyncSubscriberPolicy); - foreach (var lockKey in RegisteredLocks) + foreach (var lockKey in ExtensionsState.RegisteredLocks) { var state = factory.GetTrackerState(lockKey); var controller = factory.GetTrackerController(lockKey); @@ -182,9 +205,7 @@ public static async Task UseDistributedGrpcReactiveLockAsync( await Task.WhenAll(readySignals).ConfigureAwait(false); - IsInitializing = null; - StoredInstanceName = null; - RemoteClients = new(); + ExtensionsState = null; } } diff --git a/src/ReactiveLock.Distributed.Grpc/ReactiveLockGrpcTrackerExtensionsState.cs b/src/ReactiveLock.Distributed.Grpc/ReactiveLockGrpcTrackerExtensionsState.cs new file mode 100644 index 0000000..1fc8792 --- /dev/null +++ b/src/ReactiveLock.Distributed.Grpc/ReactiveLockGrpcTrackerExtensionsState.cs @@ -0,0 +1,34 @@ +namespace MichelOliveira.Com.ReactiveLock.Distributed.Grpc; + +using System.Collections.Concurrent; + +/// +/// Holds internal bootstrap and runtime state for gRPC-based ReactiveLock extensions. +/// +/// This class encapsulates mutable state required during the initialization and +/// lifecycle of distributed gRPC reactive locks, including: +/// +/// The current instance identifier. +/// Initialization lifecycle tracking. +/// Registered distributed lock keys. +/// Configured remote gRPC client adapters. +/// +/// +/// This type is intended for internal use only and is not part of the public API surface. +/// +/// +/// +/// ⚠️ Notice: This file is part of the ReactiveLock library and is licensed under the MIT License. +/// You must follow license, preserve the copyright notice, and comply with all legal terms +/// when using any part of this software. +/// See the LICENSE file in the project root for full license details. +/// © Michel Oliveira +/// +internal sealed class ReactiveLockGrpcTrackerExtensionsState(string instanceName) +{ + public string InstanceName { get; } = instanceName; + public bool IsInitializing { get; set; } + + public ConcurrentQueue RegisteredLocks { get; } = new(); + public List RemoteClients { get; } = new(); +} diff --git a/src/ReactiveLock.Distributed.Redis/ReactiveLock.Distributed.Redis.csproj b/src/ReactiveLock.Distributed.Redis/ReactiveLock.Distributed.Redis.csproj index 9ea0993..917f7e1 100644 --- a/src/ReactiveLock.Distributed.Redis/ReactiveLock.Distributed.Redis.csproj +++ b/src/ReactiveLock.Distributed.Redis/ReactiveLock.Distributed.Redis.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0 + net8.0;net9.0;net10.0 enable enable $(TargetsForTfmSpecificBuildOutput);IncludeProjectReferencesWithPrivateAssetsAttributeInPackage diff --git a/src/ReactiveLock.Distributed.Redis/ReactiveLockRedisTrackerExtensions.cs b/src/ReactiveLock.Distributed.Redis/ReactiveLockRedisTrackerExtensions.cs index 9a718c1..62d9faa 100644 --- a/src/ReactiveLock.Distributed.Redis/ReactiveLockRedisTrackerExtensions.cs +++ b/src/ReactiveLock.Distributed.Redis/ReactiveLockRedisTrackerExtensions.cs @@ -30,9 +30,7 @@ public static class ReactiveLockRedisTrackerExtensions private const string HASHSET_PREFIX = $"ReactiveLock:Redis:HashSet:"; private const string HASHSET_NOTIFIER_PREFIX = $"ReactiveLock:Redis:HashSetNotifier:"; - private static ConcurrentQueue<(string lockKey, string redisHashSetKey, string redisHashSetNotifierSubscriptionKey)> RegisteredLocks { get; } = new(); - private static string? StoredInstanceName { get; set; } - private static bool? IsInitializing { get; set; } + private static ReactiveLockRedisTrackerExtensionsState? ExtensionsState { get; set; } /// /// Initializes the distributed Redis reactive lock system by registering the factory @@ -44,7 +42,7 @@ public static class ReactiveLockRedisTrackerExtensions public static void InitializeDistributedRedisReactiveLock(this IServiceCollection services, string instanceName) { ReactiveLockConventions.RegisterFactory(services); - StoredInstanceName = instanceName; + ExtensionsState = string.IsNullOrEmpty(instanceName) ? null : new(instanceName); } /// @@ -88,7 +86,7 @@ public static IServiceCollection AddDistributedRedisReactiveLock( TimeSpan instanceExpirationPeriodTimeSpan, TimeSpan instanceRecoverPeriodTimeSpan) resiliencyParameters = default) { - if (string.IsNullOrEmpty(StoredInstanceName)) + if (ExtensionsState == null) { throw new InvalidOperationException( "InstanceName not initialized. Call InitializeDistributedRedisReactiveLock before adding distributed Redis reactive locks."); @@ -100,9 +98,8 @@ public static IServiceCollection AddDistributedRedisReactiveLock( ReactiveLockConventions.RegisterState(services, lockKey, onLockedHandlers, onUnlockedHandlers); ReactiveLockConventions.RegisterController(services, lockKey, (sp) => { - var isInitializing = IsInitializing.HasValue && IsInitializing.Value; - var isNotInitializing = !isInitializing; - var hasPendingLockRegistrations = !RegisteredLocks.IsEmpty; + var isNotInitializing = !ExtensionsState.IsInitializing; + var hasPendingLockRegistrations = !ExtensionsState.RegisteredLocks.IsEmpty; if (isNotInitializing && hasPendingLockRegistrations) { @@ -112,15 +109,15 @@ public static IServiceCollection AddDistributedRedisReactiveLock( on your IApplicationBuilder instance after 'var app = builder.Build();'."); } var redis = sp.GetRequiredService(); - var store = new ReactiveLockRedisTrackerStore(redis, StoredInstanceName, - customAsyncStorePolicy, + var store = new ReactiveLockRedisTrackerStore(redis, ExtensionsState.InstanceName, + customAsyncStorePolicy, resiliencyParameters, redisHashSetKey, redisHashSetNotifierKey); - + return new ReactiveLockTrackerController(store, busyThreshold); }); - RegisteredLocks.Enqueue((lockKey, redisHashSetKey, redisHashSetNotifierKey)); + ExtensionsState.RegisteredLocks.Enqueue((lockKey, redisHashSetKey, redisHashSetNotifierKey)); return services; } @@ -139,13 +136,20 @@ public static async Task UseDistributedRedisReactiveLockAsync( this IApplicationBuilder application, IAsyncPolicy? customAsyncSubscriberPolicy = default) { - IsInitializing = true; + + if (ExtensionsState == null) + { + throw new InvalidOperationException( + "InstanceName not initialized. Call InitializeDistributedRedisReactiveLock before adding distributed Redis reactive locks."); + } + + ExtensionsState.IsInitializing = true; var redis = application.ApplicationServices.GetRequiredService(); var redisDb = redis.GetDatabase(); var subscriber = redis.GetSubscriber(); var retryPolicy = ReactiveLockPollyPolicies.UseOrCreateDefaultRetryPolicy(customAsyncSubscriberPolicy); - while (RegisteredLocks.TryDequeue(out var lockInfo)) + while (ExtensionsState.RegisteredLocks.TryDequeue(out var lockInfo)) { var (lockKey, redisHashSetKey, redisHashSetNotifierSubscriptionKey) = lockInfo; @@ -172,8 +176,7 @@ await retryPolicy.ExecuteAsync(async () => }).ConfigureAwait(false); }); } - IsInitializing = null; - StoredInstanceName = null; + ExtensionsState = null; } } diff --git a/src/ReactiveLock.Distributed.Redis/ReactiveLockRedisTrackerExtensionsState.cs b/src/ReactiveLock.Distributed.Redis/ReactiveLockRedisTrackerExtensionsState.cs new file mode 100644 index 0000000..9eba57c --- /dev/null +++ b/src/ReactiveLock.Distributed.Redis/ReactiveLockRedisTrackerExtensionsState.cs @@ -0,0 +1,35 @@ +namespace MichelOliveira.Com.ReactiveLock.Distributed.Redis; + +using System.Collections.Concurrent; + +/// +/// Holds internal bootstrap and runtime state for Redis-based ReactiveLock extensions. +/// +/// This class encapsulates mutable state required during the initialization and +/// lifecycle of distributed Redis reactive locks, including: +/// +/// The current instance identifier. +/// Initialization lifecycle tracking. +/// Registered distributed lock metadata. +/// +/// +/// This type is intended for internal use only and is not part of the public API surface. +/// +/// +/// +/// ⚠️ Notice: This file is part of the ReactiveLock library and is licensed under the MIT License. +/// You must follow license, preserve the copyright notice, and comply with all legal terms +/// when using any part of this software. +/// See the LICENSE file in the project root for full license details. +/// © Michel Oliveira +/// +internal sealed class ReactiveLockRedisTrackerExtensionsState(string instanceName) +{ + public string InstanceName { get; } = instanceName; + public bool IsInitializing { get; set; } + + public ConcurrentQueue<( + string lockKey, + string redisHashSetKey, + string redisHashSetNotifierSubscriptionKey)> RegisteredLocks { get; } = new(); +} \ No newline at end of file diff --git a/src/ReactiveLock.Shared.Distributed/ReactiveLock.Shared.Distributed.csproj b/src/ReactiveLock.Shared.Distributed/ReactiveLock.Shared.Distributed.csproj index a4b6274..ad28da9 100644 --- a/src/ReactiveLock.Shared.Distributed/ReactiveLock.Shared.Distributed.csproj +++ b/src/ReactiveLock.Shared.Distributed/ReactiveLock.Shared.Distributed.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0 + net8.0;net9.0;net10.0 enable enable diff --git a/src/ReactiveLock.Tests/ReactiveLock.Tests.csproj b/src/ReactiveLock.Tests/ReactiveLock.Tests.csproj index 3625676..9ebd71e 100644 --- a/src/ReactiveLock.Tests/ReactiveLock.Tests.csproj +++ b/src/ReactiveLock.Tests/ReactiveLock.Tests.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0 + net8.0;net9.0;net10.0 enable enable false diff --git a/src/ReactiveLock.Tests/ReactiveLockGrpcTrackerExtensionsTests.cs b/src/ReactiveLock.Tests/ReactiveLockGrpcTrackerExtensionsTests.cs index 9748a24..7ba6a0a 100644 --- a/src/ReactiveLock.Tests/ReactiveLockGrpcTrackerExtensionsTests.cs +++ b/src/ReactiveLock.Tests/ReactiveLockGrpcTrackerExtensionsTests.cs @@ -79,49 +79,35 @@ private static AsyncDuplexStreamingCall(); - - // assign a new list with your mock inside - typeof(ReactiveLockGrpcTrackerExtensions) - .GetProperty("RemoteClients", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)! - .SetValue(null, new List { clientMock.Object }); - - typeof(ReactiveLockGrpcTrackerExtensions) - .GetProperty("StoredInstanceName", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)! - .SetValue(null, "instance-x"); - - // Enqueue a pending lock to simulate uninitialized state - var queue = (ConcurrentQueue)typeof(ReactiveLockGrpcTrackerExtensions) - .GetProperty("RegisteredLocks", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)! - .GetValue(null)!; + // Arrange + var services = new ServiceCollection(); - queue.Enqueue("lock-y"); + var clientMock = new Mock(); - var services = new ServiceCollection(); + // capability-based initialization + services.InitializeDistributedGrpcReactiveLock( + instanceName: "instance-x", + remoteClients: new[] { clientMock.Object } + ); - ReactiveLockConventions.RegisterFactory(services); services.AddDistributedGrpcReactiveLock("lock-y"); var provider = services.BuildServiceProvider(); - var factory = provider.GetRequiredService(); - // Act & Assert: resolving the controller triggers the exception + // Act & Assert var ex = Assert.Throws(() => factory.GetTrackerController("lock-y")); - Assert.Contains("Distributed Grpc reactive locks are not initialized", ex.Message); - - // Cleanup - queue.TryDequeue(out _); + Assert.Contains( + "Distributed Grpc reactive locks are not initialized", + ex.Message + ); } - - [Fact] public void AddDistributedGrpcReactiveLock_WhenNotInitialized_Throws() { @@ -129,27 +115,38 @@ public void AddDistributedGrpcReactiveLock_WhenNotInitialized_Throws() var clientMock = new Mock(); - // assign a new list with your mock inside - typeof(ReactiveLockGrpcTrackerExtensions) - .GetProperty("RemoteClients", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)! - .SetValue(null, new List { clientMock.Object }); - - typeof(ReactiveLockGrpcTrackerExtensions) - .GetProperty("StoredInstanceName", BindingFlags.NonPublic | BindingFlags.Static)!.SetValue(null, null); + services.InitializeDistributedGrpcReactiveLock(string.Empty, Array.Empty()); Assert.Throws(() => services.AddDistributedGrpcReactiveLock("lock-x")); } - + [Fact] public async Task UseDistributedGrpcReactiveLockAsync_ProcessesUpdates() { // Arrange var services = new ServiceCollection(); - services.InitializeDistributedGrpcReactiveLock("instance-x", "http://localhost:5000"); + + // ---- mock gRPC duplex call using Channels + var duplexCall = CreateMockDuplexCall(out var responseChannel); + + var clientMock = new Mock(); + clientMock + .Setup(c => c.SubscribeLockStatus( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(duplexCall); + + // ---- initialize via capability (NO reflection) + services.InitializeDistributedGrpcReactiveLock( + instanceName: "instance-x", + remoteClients: new[] { clientMock.Object } + ); + services.AddDistributedGrpcReactiveLock("lock-x"); - // Mock state and controller + // ---- mock state + controller var stateMock = new Mock(); var tcsBlocked = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -163,61 +160,59 @@ public async Task UseDistributedGrpcReactiveLockAsync_ProcessesUpdates() .Returns(Task.CompletedTask); var controllerMock = new Mock(); - controllerMock.Setup(c => c.DecrementAsync(It.IsAny())).Returns(Task.CompletedTask); + controllerMock + .Setup(c => c.DecrementAsync(It.IsAny())) + .Returns(Task.CompletedTask); - // Mock factory + // ---- mock factory var factoryMock = new Mock(); factoryMock.Setup(f => f.GetTrackerState("lock-x")).Returns(stateMock.Object); factoryMock.Setup(f => f.GetTrackerController("lock-x")).Returns(controllerMock.Object); services.AddSingleton(factoryMock.Object); + var provider = services.BuildServiceProvider(); var appMock = new Mock(); appMock.Setup(a => a.ApplicationServices).Returns(provider); - // Mock gRPC duplex call using Channels - var duplexCall = CreateMockDuplexCall(out var responseChannel); - - var clientMock = new Mock(); - clientMock - .Setup(c => c.SubscribeLockStatus(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(duplexCall); - - - // assign a new list with your mock inside - typeof(ReactiveLockGrpcTrackerExtensions) - .GetProperty("RemoteClients", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)! - .SetValue(null, new List { clientMock.Object }); - - - typeof(ReactiveLockGrpcTrackerExtensions) - .GetProperty("StoredInstanceName", BindingFlags.NonPublic | BindingFlags.Static)! - .SetValue(null, "instance-x"); - // Act - var task = ReactiveLockGrpcTrackerExtensions.UseDistributedGrpcReactiveLockAsync(appMock.Object); + var task = ReactiveLockGrpcTrackerExtensions + .UseDistributedGrpcReactiveLockAsync(appMock.Object); - // Trigger a blocked state notification + // ---- trigger a blocked notification await responseChannel.Writer.WriteAsync(new LockStatusNotification { - InstancesStatus = { { "instance-x", new InstanceLockStatus { IsBusy = true, LockData = "data1", - ValidUntil = Timestamp.FromDateTimeOffset( - DateTimeOffset.UtcNow.AddSeconds(10)) } } } + InstancesStatus = + { + { + "instance-x", + new InstanceLockStatus + { + IsBusy = true, + LockData = "data1", + ValidUntil = Timestamp.FromDateTimeOffset( + DateTimeOffset.UtcNow.AddSeconds(10)) + } + } + } }); - // Complete the channel to signal no more notifications responseChannel.Writer.Complete(); - // Wait until the blocked callback is invoked + // Wait until callback fires await tcsBlocked.Task; - // Wait for the main async method to finish + // Wait for subscription bootstrap to finish await task; - // Assert that blocked/unblocked methods were called - stateMock.Verify(s => s.SetLocalStateBlockedAsync("data1"), Times.Once); + // Assert + stateMock.Verify( + s => s.SetLocalStateBlockedAsync("data1"), + Times.Once + ); } + [Fact] public void ReactiveLockGrpcTrackerStore_AreAllIdleTests() { diff --git a/src/ReactiveLock.Tests/ReactiveLockRedisTrackerExtensions.cs b/src/ReactiveLock.Tests/ReactiveLockRedisTrackerExtensions.cs index 5249959..c1eaf31 100644 --- a/src/ReactiveLock.Tests/ReactiveLockRedisTrackerExtensions.cs +++ b/src/ReactiveLock.Tests/ReactiveLockRedisTrackerExtensions.cs @@ -14,20 +14,13 @@ namespace ReactiveLock.Tests; public class ReactiveLockRedisTrackerExtensionsTests { - private static void ResetStaticState() - { - typeof(ReactiveLockRedisTrackerExtensions) - .GetProperty("StoredInstanceName", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)! - .SetValue(null, null); - } - [Fact] public void AddDistributedRedisReactiveLock_Throws_IfNotInitialized() { - ResetStaticState(); - var services = new ServiceCollection(); + services.InitializeDistributedRedisReactiveLock(string.Empty); + var ex = Assert.Throws(() => services.AddDistributedRedisReactiveLock("some-lock") ); @@ -36,7 +29,7 @@ public void AddDistributedRedisReactiveLock_Throws_IfNotInitialized() } [Fact] - public void AddDistributedRedisReactiveLock_RegistersDependencies() + public async Task AddDistributedRedisReactiveLock_RegistersDependencies() { var services = new ServiceCollection(); @@ -48,7 +41,7 @@ public void AddDistributedRedisReactiveLock_RegistersDependencies() services.AddDistributedRedisReactiveLock("lock-x"); var provider = services.BuildServiceProvider(); - + // assert no registration failure Assert.NotNull(provider.GetService()); } diff --git a/test/integration/dotnet-csharp-grpc/src/backend/Dockerfile b/test/integration/dotnet-csharp-grpc/src/backend/Dockerfile index e6e3faa..276e1e4 100644 --- a/test/integration/dotnet-csharp-grpc/src/backend/Dockerfile +++ b/test/integration/dotnet-csharp-grpc/src/backend/Dockerfile @@ -6,14 +6,14 @@ ARG LAUNCHING_FROM_VS ARG FINAL_BASE_IMAGE=${LAUNCHING_FROM_VS:+aotdebug} # This stage is used when running from VS in fast mode (Default for Debug configuration) -FROM mcr.microsoft.com/dotnet/aspnet:10.0.100-preview.7-alpine3.22-aot AS base +FROM mcr.microsoft.com/dotnet/aspnet:10.0.2-alpine3.22 AS base USER $APP_UID WORKDIR /app EXPOSE 8080 8081 # This stage is used to build the service project -FROM mcr.microsoft.com/dotnet/sdk:10.0.100-preview.7-alpine3.22-aot AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0.102-alpine3.22-aot AS build # Install clang/zlib1g-dev dependencies for publishing to native RUN apk update \ && apk add build-base zlib-dev grpc-plugins @@ -49,7 +49,7 @@ RUN apk update \ USER app # This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration) -FROM ${FINAL_BASE_IMAGE:-mcr.microsoft.com/dotnet/runtime-deps:10.0.0-preview.7-alpine3.22} AS final +FROM ${FINAL_BASE_IMAGE:-mcr.microsoft.com/dotnet/runtime-deps:10.0.2-alpine3.22} AS final WORKDIR /app EXPOSE 8080 8081 COPY --from=publish /app/publish . diff --git a/test/integration/dotnet-csharp-grpc/src/backend/backend.csproj b/test/integration/dotnet-csharp-grpc/src/backend/backend.csproj index 0f97277..63fcaa9 100644 --- a/test/integration/dotnet-csharp-grpc/src/backend/backend.csproj +++ b/test/integration/dotnet-csharp-grpc/src/backend/backend.csproj @@ -12,7 +12,7 @@ - + diff --git a/test/integration/dotnet-csharp-grpc/src/haproxy.cfg b/test/integration/dotnet-csharp-grpc/src/haproxy.cfg index b644e02..59c0a01 100644 --- a/test/integration/dotnet-csharp-grpc/src/haproxy.cfg +++ b/test/integration/dotnet-csharp-grpc/src/haproxy.cfg @@ -1,7 +1,7 @@ global nbthread 1 maxconn 4444 - ulimit-n 555555 + ulimit-n 65000 defaults mode http diff --git a/test/integration/dotnet-csharp-redis/src/backend/Dockerfile b/test/integration/dotnet-csharp-redis/src/backend/Dockerfile index fafe4d3..11a7a65 100644 --- a/test/integration/dotnet-csharp-redis/src/backend/Dockerfile +++ b/test/integration/dotnet-csharp-redis/src/backend/Dockerfile @@ -6,14 +6,14 @@ ARG LAUNCHING_FROM_VS ARG FINAL_BASE_IMAGE=${LAUNCHING_FROM_VS:+aotdebug} # This stage is used when running from VS in fast mode (Default for Debug configuration) -FROM mcr.microsoft.com/dotnet/aspnet:10.0.100-preview.7-alpine3.22-aot AS base +FROM mcr.microsoft.com/dotnet/aspnet:10.0.2-alpine3.22 AS base USER $APP_UID WORKDIR /app EXPOSE 8080 # This stage is used to build the service project -FROM mcr.microsoft.com/dotnet/sdk:10.0.100-preview.7-alpine3.22-aot AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0.102-alpine3.22-aot AS build # Install clang/zlib1g-dev dependencies for publishing to native RUN apk update \ && apk add build-base zlib-dev @@ -47,7 +47,7 @@ RUN apk update \ USER app # This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration) -FROM ${FINAL_BASE_IMAGE:-mcr.microsoft.com/dotnet/runtime-deps:10.0.0-preview.7-alpine3.22} AS final +FROM ${FINAL_BASE_IMAGE:-mcr.microsoft.com/dotnet/runtime-deps:10.0.2-alpine3.22} AS final WORKDIR /app EXPOSE 8080 COPY --from=publish /app/publish . diff --git a/test/integration/dotnet-csharp-redis/src/backend/Service/PaymentSummaryService.cs b/test/integration/dotnet-csharp-redis/src/backend/Service/PaymentSummaryService.cs index 44df4c5..0ffa8d1 100644 --- a/test/integration/dotnet-csharp-redis/src/backend/Service/PaymentSummaryService.cs +++ b/test/integration/dotnet-csharp-redis/src/backend/Service/PaymentSummaryService.cs @@ -50,7 +50,7 @@ await WaitWithTimeoutAsync(async () => var redisValues = await RedisDb.ListRangeAsync(Constant.REDIS_PAYMENTS_BATCH_KEY).ConfigureAwait(false); var payments = redisValues - .Select(v => JsonSerializer.Deserialize(v!, JsonContext.Default.PaymentInsertParameters)) + .Select(v => JsonSerializer.Deserialize((byte[])v!, JsonContext.Default.PaymentInsertParameters)) .Where(p => p != null) .Where(p => (!from.HasValue || p?.RequestedAt >= from) && diff --git a/test/integration/dotnet-csharp-redis/src/backend/backend.csproj b/test/integration/dotnet-csharp-redis/src/backend/backend.csproj index 184c4f5..67963bb 100644 --- a/test/integration/dotnet-csharp-redis/src/backend/backend.csproj +++ b/test/integration/dotnet-csharp-redis/src/backend/backend.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/integration/dotnet-csharp-redis/src/backend/backend.http b/test/integration/dotnet-csharp-redis/src/backend/backend.http index fba06bb..3fb71eb 100644 --- a/test/integration/dotnet-csharp-redis/src/backend/backend.http +++ b/test/integration/dotnet-csharp-redis/src/backend/backend.http @@ -5,7 +5,7 @@ POST {{backend_HostAddress}}/payments Content-Type: application/json { - "correlationId": "123e4567-e89b-12d3-a456-42661417400b", + "correlationId": "123e4567-e89b-12d3-a456-42661417400c", "amount": 234.56 } diff --git a/test/integration/dotnet-csharp-redis/src/haproxy.cfg b/test/integration/dotnet-csharp-redis/src/haproxy.cfg index b644e02..59c0a01 100644 --- a/test/integration/dotnet-csharp-redis/src/haproxy.cfg +++ b/test/integration/dotnet-csharp-redis/src/haproxy.cfg @@ -1,7 +1,7 @@ global nbthread 1 maxconn 4444 - ulimit-n 555555 + ulimit-n 65000 defaults mode http