Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a9353d6
feat: better nuget login
micheloliveira-com Sep 26, 2025
0565b2a
feat: improve dependency injection strategy by introducing ReactiveLo…
micheloliveira-com Feb 2, 2026
90c9b93
feat: refactor dependency injection strategy by introducing ReactiveL…
micheloliveira-com Feb 2, 2026
10107e5
fix: correct namespace declaration in ReactiveLockGrpcTrackerExtensio…
micheloliveira-com Feb 2, 2026
4a841e3
feat: update InitializeDistributedGrpcReactiveLock to accept params f…
micheloliveira-com Feb 2, 2026
1f03d7b
feat: upgrade target frameworks to .NET 10.0 across all projects
micheloliveira-com Feb 2, 2026
e26e056
fix: update Microsoft.Extensions.Http.Polly package version to 10.0.0
micheloliveira-com Feb 2, 2026
7f48d55
feat: upgrade to .NET 10 in CI workflows
micheloliveira-com Feb 3, 2026
41ec0bf
feat: enhance InitializeDistributedGrpcReactiveLock methods to valida…
micheloliveira-com Feb 3, 2026
4c50f0e
Merge pull request #37 from micheloliveira-com/feature/upgrade-to-dot…
micheloliveira-com Feb 3, 2026
b84517e
Merge pull request #36 from micheloliveira-com/feature/improve-depend…
micheloliveira-com Feb 3, 2026
957cbc9
Merge branch 'develop' into feature/upgrade-integration-tests-to-dotn…
micheloliveira-com Feb 3, 2026
090aa71
feat: update Dockerfiles to use .NET 10.0.2 images for consistency an…
micheloliveira-com Feb 3, 2026
a81a128
feat: ensure comment.md generation and upload steps always execute
micheloliveira-com Feb 3, 2026
d122351
feat: enhance comment.md generation to handle missing k6 results file
micheloliveira-com Feb 3, 2026
9190924
fix: correct deserialization of Redis values in PaymentSummaryService
micheloliveira-com Feb 3, 2026
15b5671
refactor: update Docker Compose commands to use the new syntax and re…
micheloliveira-com Feb 3, 2026
f5df5d2
feat: update default NuGet package version to 10.1.0-beta.1 in k6-tes…
micheloliveira-com Feb 3, 2026
bdd8483
fix: correct path for appending Docker Compose logs to comment.md
micheloliveira-com Feb 3, 2026
cde463c
fix: reduce attempts in health check loop and update Docker Compose l…
micheloliveira-com Feb 3, 2026
a9606b0
fix: increase health check attempts and update Docker Compose logs co…
micheloliveira-com Feb 3, 2026
283018b
fix: update ulimit-n value to 65000 in HAProxy configuration
micheloliveira-com Feb 3, 2026
12999b1
Merge pull request #38 from micheloliveira-com/feature/upgrade-integr…
micheloliveira-com Feb 3, 2026
0d4feb0
Merge branch 'main' into develop
micheloliveira-com Feb 3, 2026
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: 9 additions & 5 deletions .github/workflows/k6-test-multi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ name: k6 Test with Publish - Preview & Final
permissions:
contents: read
actions: read
id-token: write

on:
push:
Expand All @@ -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:
Expand Down Expand Up @@ -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..."
Expand Down
29 changes: 17 additions & 12 deletions .github/workflows/k6-test-single.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |

Expand Down Expand Up @@ -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: |
Expand Down Expand Up @@ -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
Comment thread
micheloliveira-com marked this conversation as resolved.

- 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 }}
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/pack-nupkgs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/sonarqube.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/ReactiveLock.Core/ReactiveLock.Core.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);IncludeProjectReferencesWithPrivateAssetsAttributeInPackage</TargetsForTfmSpecificBuildOutput>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);IncludeProjectReferencesWithPrivateAssetsAttributeInPackage</TargetsForTfmSpecificBuildOutput>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,42 @@
/// </summary>
public static class ReactiveLockGrpcTrackerExtensions
{
private static bool? IsInitializing { get; set; }
private static ConcurrentQueue<string> RegisteredLocks { get; } = new();
private static string? StoredInstanceName { get; set; }
private static List<IReactiveLockGrpcClientAdapter> 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))
)
)
);
}

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);
}

/// <summary>
/// Registers distributed gRPC reactive lock services, configuring lock state, controller, and handlers.
/// </summary>
Expand Down Expand Up @@ -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.");
Expand All @@ -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)
{
Expand All @@ -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,
Expand Down Expand Up @@ -157,16 +175,21 @@ public static async Task UseDistributedGrpcReactiveLockAsync(
this IApplicationBuilder app,
IAsyncPolicy? customAsyncSubscriberPolicy = default)
{
IsInitializing = true;
var factory = app.ApplicationServices.GetRequiredService<IReactiveLockTrackerFactory>();
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<IReactiveLockTrackerFactory>();

var instanceStoredInstanceName = ExtensionsState.InstanceName;
var instanceRemoteClients = ExtensionsState.RemoteClients;
var readySignals = new List<Task>();
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);
Expand All @@ -182,9 +205,7 @@ public static async Task UseDistributedGrpcReactiveLockAsync(

await Task.WhenAll(readySignals).ConfigureAwait(false);

IsInitializing = null;
StoredInstanceName = null;
RemoteClients = new();
ExtensionsState = null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace MichelOliveira.Com.ReactiveLock.Distributed.Grpc;

using System.Collections.Concurrent;

/// <summary>
/// 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:
/// <list type="bullet">
/// <item><description>The current instance identifier.</description></item>
/// <item><description>Initialization lifecycle tracking.</description></item>
/// <item><description>Registered distributed lock keys.</description></item>
/// <item><description>Configured remote gRPC client adapters.</description></item>
/// </list>
///
/// This type is intended for internal use only and is not part of the public API surface.
/// </summary>
///
/// <para>
/// ⚠️ 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
/// </para>
internal sealed class ReactiveLockGrpcTrackerExtensionsState(string instanceName)
{
public string InstanceName { get; } = instanceName;
public bool IsInitializing { get; set; }

public ConcurrentQueue<string> RegisteredLocks { get; } = new();
public List<IReactiveLockGrpcClientAdapter> RemoteClients { get; } = new();
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);IncludeProjectReferencesWithPrivateAssetsAttributeInPackage</TargetsForTfmSpecificBuildOutput>
Expand Down
Loading
Loading