Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
470bee5
add nacose
MiaoShuYo Mar 19, 2025
643c478
Block folder. idea
MiaoShuYo Mar 19, 2025
732fec4
delete .idea
MiaoShuYo Mar 19, 2025
7899622
Update .gitignore
MiaoShuYo Mar 19, 2025
589cf4d
Migrate file location, delete subfolders
MiaoShuYo Mar 19, 2025
2100626
Add blank lines
MiaoShuYo Mar 19, 2025
ffbf1b5
Migrate test project location.
MiaoShuYo Mar 19, 2025
f39757d
Organize solution correctly
raman-m Mar 19, 2025
30a0f34
Use file-scoped namespaces
raman-m Mar 19, 2025
0a3f426
CI/CD: add workflows aka GitHub Actions
raman-m Mar 19, 2025
306196c
Modified code
MiaoShuYo Mar 20, 2025
31308ca
Update Ocelot.Discovery.Nacos.UnitTests.csproj
MiaoShuYo Mar 20, 2025
1eed642
Update pr.yml
MiaoShuYo Mar 20, 2025
de2b498
Fix warning
MiaoShuYo Mar 20, 2025
ece0185
Fix warning
MiaoShuYo Mar 20, 2025
71d81bc
Fix VS IDE messages
raman-m Mar 21, 2025
c7c0460
Test coverage 100%
MiaoShuYo Mar 21, 2025
d7c7c33
Review tests
raman-m Mar 22, 2025
d962f9e
No anonymous delegates: use named delegate
raman-m Mar 22, 2025
b82594c
Replace the log component with IOcelotLoggerFactory
MiaoShuYo Mar 22, 2025
6767b6e
Remove classes that are no longer on trial.
MiaoShuYo Mar 24, 2025
4547c55
Add detailed notes to _reservedKeys
MiaoShuYo Mar 24, 2025
bea32f4
Code review by @raman-m
raman-m Mar 24, 2025
7912f62
Add acceptance testing project
raman-m Mar 24, 2025
7ac2cae
Update workflows to run acceptance tests
raman-m Mar 24, 2025
1a16f83
Update src/NacosMiddlewareConfigurationProvider.cs
raman-m Mar 24, 2025
eec6bad
_reservedKeys changed to public
MiaoShuYo Mar 24, 2025
950c5ae
Temporarily disable acceptance testing due to the draft status of the…
raman-m May 27, 2025
dd05752
Update package references
raman-m May 27, 2025
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
30 changes: 30 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This workflow will build a .NET project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net

name: Main

on:
push:
branches: [ "main" ]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
dotnet-version: [ '8.0', '9.0' ]

steps:
- uses: actions/checkout@v4
- name: Setup .NET ${{ matrix.dotnet-version }}
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ matrix.dotnet-version }}.x
- name: Restore
run: dotnet restore ./Ocelot.Discovery.Nacos.sln -p:TargetFramework=net${{ matrix.dotnet-version }}
- name: Build
run: dotnet build --no-restore ./Ocelot.Discovery.Nacos.sln --framework net${{ matrix.dotnet-version }}
- name: Unit Tests
run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }} ./unit/Ocelot.Discovery.Nacos.UnitTests.csproj
# - name: Acceptance Tests
# run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }} ./acceptance/Ocelot.Discovery.Nacos.AcceptanceTests.csproj
24 changes: 24 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# This workflow will build a .NET project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net

name: PR

on: pull_request

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET 8.0
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Restore
run: dotnet restore ./Ocelot.Discovery.Nacos.sln -p:TargetFramework=net8.0
- name: Build
run: dotnet build --no-restore ./Ocelot.Discovery.Nacos.sln --framework net8.0
- name: Unit Tests
run: dotnet test --no-restore --no-build --verbosity normal --framework net8.0 ./unit/Ocelot.Discovery.Nacos.UnitTests.csproj
# - name: Acceptance Tests
# run: dotnet test --no-restore --no-build --verbosity normal --framework net8.0 ./acceptance/Ocelot.Discovery.Nacos.AcceptanceTests.csproj
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,5 @@ FodyWeavers.xsd

# JetBrains Rider
*.sln.iml
.idea/*
/Ocelot.Discovery.Nacos/.idea
43 changes: 43 additions & 0 deletions Ocelot.Discovery.Nacos.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35527.113
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Discovery.Nacos", "src\Ocelot.Discovery.Nacos.csproj", "{CEF24699-3E41-D971-ACCA-FEF5CCB2011C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Discovery.Nacos.UnitTests", "unit\Ocelot.Discovery.Nacos.UnitTests.csproj", "{FAD17C0B-4F8F-99A6-1419-9665E4210346}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Discovery.Nacos.AcceptanceTests", "acceptance\Ocelot.Discovery.Nacos.AcceptanceTests.csproj", "{D14C0D65-EDF4-42DA-8028-948E6A15F41B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
ProjectSection(SolutionItems) = preProject
.github\workflows\main.yml = .github\workflows\main.yml
.github\workflows\pr.yml = .github\workflows\pr.yml
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CEF24699-3E41-D971-ACCA-FEF5CCB2011C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CEF24699-3E41-D971-ACCA-FEF5CCB2011C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CEF24699-3E41-D971-ACCA-FEF5CCB2011C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CEF24699-3E41-D971-ACCA-FEF5CCB2011C}.Release|Any CPU.Build.0 = Release|Any CPU
{FAD17C0B-4F8F-99A6-1419-9665E4210346}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FAD17C0B-4F8F-99A6-1419-9665E4210346}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FAD17C0B-4F8F-99A6-1419-9665E4210346}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FAD17C0B-4F8F-99A6-1419-9665E4210346}.Release|Any CPU.Build.0 = Release|Any CPU
{D14C0D65-EDF4-42DA-8028-948E6A15F41B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D14C0D65-EDF4-42DA-8028-948E6A15F41B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D14C0D65-EDF4-42DA-8028-948E6A15F41B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D14C0D65-EDF4-42DA-8028-948E6A15F41B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0E583663-7152-4575-9AE5-FF3D78AEAA1C}
EndGlobalSection
EndGlobal
273 changes: 273 additions & 0 deletions acceptance/ConcurrentSteps.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Ocelot.Discovery.Nacos.AcceptanceTests.LoadBalancer;
using Ocelot.LoadBalancer;
using Shouldly;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net;
using System.Runtime.CompilerServices;
using System.Text;

namespace Ocelot.Discovery.Nacos.AcceptanceTests;

public class ConcurrentSteps : Steps, IDisposable
{
protected Task[] _tasks;
protected ServiceHandler[] _handlers;
protected ConcurrentDictionary<int, HttpResponseMessage> _responses;
protected volatile int[] _counters;

public ConcurrentSteps()
{
_tasks = Array.Empty<Task>();
_handlers = Array.Empty<ServiceHandler>();
_responses = new();
_counters = Array.Empty<int>();
}

public override void Dispose()
{
foreach (var handler in _handlers)
{
handler?.Dispose();
}

foreach (var response in _responses.Values)
{
response?.Dispose();
}

foreach (var task in _tasks)
{
task?.Dispose();
}

base.Dispose();
GC.SuppressFinalize(this);
}

protected void GivenServiceInstanceIsRunning(string url, string response)
=> GivenServiceInstanceIsRunning(url, response, HttpStatusCode.OK);

protected void GivenServiceInstanceIsRunning(string url, string response, HttpStatusCode statusCode)
{
_handlers = new ServiceHandler[1]; // allocate single instance
_counters = new int[1]; // single counter
GivenServiceIsRunning(url, response, 0, statusCode);
_counters[0] = 0;
}

protected void GivenThereIsAServiceRunningOn(string url, string basePath, string responseBody)
{
var handler = new ServiceHandler();
_handlers = new ServiceHandler[] { handler };
handler.GivenThereIsAServiceRunningOn(url, basePath, MapGet(basePath, responseBody));
}

protected void GivenMultipleServiceInstancesAreRunning(string[] urls, [CallerMemberName] string serviceName = null)

Check warning on line 68 in acceptance/ConcurrentSteps.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.
{
serviceName ??= new Uri(urls[0]).Host;
string[] responses = urls.Select(u => $"{serviceName}|url({u})").ToArray();
GivenMultipleServiceInstancesAreRunning(urls, responses, HttpStatusCode.OK);
}

protected void GivenMultipleServiceInstancesAreRunning(string[] urls, string[] responses)
=> GivenMultipleServiceInstancesAreRunning(urls, responses, HttpStatusCode.OK);

protected void GivenMultipleServiceInstancesAreRunning(string[] urls, string[] responses, HttpStatusCode statusCode)
{
Debug.Assert(urls.Length == responses.Length, "Length mismatch!");
_handlers = new ServiceHandler[urls.Length]; // allocate multiple instances
_counters = new int[urls.Length]; // multiple counters
for (int i = 0; i < urls.Length; i++)
{
GivenServiceIsRunning(urls[i], responses[i], i, statusCode);
_counters[i] = 0;
}
}

private void GivenServiceIsRunning(string url, string response)
=> GivenServiceIsRunning(url, response, 0, HttpStatusCode.OK);
private void GivenServiceIsRunning(string url, string response, int index)
=> GivenServiceIsRunning(url, response, index, HttpStatusCode.OK);

private void GivenServiceIsRunning(string url, string response, int index, HttpStatusCode successCode)
{
response ??= successCode.ToString();
_handlers[index] ??= new();
var serviceHandler = _handlers[index];
serviceHandler.GivenThereIsAServiceRunningOn(url, MapGet(index, response, successCode));
}

protected static RequestDelegate MapGet(string path, string responseBody) => MapGet(path, responseBody, HttpStatusCode.OK);
protected static RequestDelegate MapGet(string path, string responseBody, HttpStatusCode statusCode) => async context =>
{
var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value)
? context.Request.PathBase.Value
: context.Request.Path.Value;
bool isMatch = downstreamPath == path;
context.Response.StatusCode = (int)(isMatch ? statusCode : HttpStatusCode.NotFound);
await context.Response.WriteAsync(isMatch ? responseBody : "Not Found");
};

public static class HeaderNames
{
public const string ServiceIndex = nameof(LeaseEventArgs.ServiceIndex);
public const string Host = nameof(Uri.Host);
public const string Port = nameof(Uri.Port);
public const string Counter = nameof(Counter);
}

protected RequestDelegate MapGet(int index, string body) => MapGet(index, body, HttpStatusCode.OK);
protected RequestDelegate MapGet(int index, string body, HttpStatusCode successCode) => async context =>
{
// Don't delay during the first service call
if (Volatile.Read(ref _counters[index]) > 0)
{
await Task.Delay(Random.Shared.Next(5, 15)); // emulate integration delay up to 15 milliseconds
}

string responseBody;
var request = context.Request;
var response = context.Response;
try
{
int count = Interlocked.Increment(ref _counters[index]);
responseBody = string.Concat(count, ':', body);

response.StatusCode = (int)successCode;
response.Headers.Append(HeaderNames.ServiceIndex, new StringValues(index.ToString()));
response.Headers.Append(HeaderNames.Host, new StringValues(request.Host.Host));
response.Headers.Append(HeaderNames.Port, new StringValues(request.Host.Port.ToString()));
response.Headers.Append(HeaderNames.Counter, new StringValues(count.ToString()));
await response.WriteAsync(responseBody);
}
catch (Exception exception)
{
responseBody = string.Concat(1, ':', exception.StackTrace);
response.StatusCode = (int)HttpStatusCode.InternalServerError;
await response.WriteAsync(responseBody);
}
};

public Task[] WhenIGetUrlOnTheApiGatewayConcurrently(string url, int times)
=> RunParallelRequests(times, (i) => url);

public Task[] WhenIGetUrlOnTheApiGatewayConcurrently(int times, params string[] urls)
=> RunParallelRequests(times, (i) => urls[i % urls.Length]);

protected Task[] RunParallelRequests(int times, Func<int, string> urlFunc)
{
_tasks = new Task[times];
_responses = new(times, times);
for (var i = 0; i < times; i++)
{
var url = urlFunc(i);
_tasks[i] = GetParallelResponse(url, i);
_responses[i] = null;
}

Task.WaitAll(_tasks);
return _tasks;
}

private async Task GetParallelResponse(string url, int threadIndex)
{
var response = await _ocelotClient.GetAsync(url);
var content = await response.Content.ReadAsStringAsync();
var counterString = content.Contains(':')
? content.Split(':')[0] // let the first fragment is counter value
: "0";
int count = int.Parse(counterString);
count.ShouldBeGreaterThan(0);
_responses[threadIndex] = response;
}

public void ThenAllStatusCodesShouldBe(HttpStatusCode expected)
=> _responses.ShouldAllBe(response => response.Value.StatusCode == expected);
public void ThenAllResponseBodiesShouldBe(string expectedBody)
=> _responses.ShouldAllBe(response => response.Value.Content.ReadAsStringAsync().Result == expectedBody);

protected string CalledTimesMessage()
=> $"All values are [{string.Join(',', _counters)}]";

public void ThenAllServicesShouldHaveBeenCalledTimes(int expected)
=> _counters.Sum().ShouldBe(expected, CalledTimesMessage());

public void ThenServiceShouldHaveBeenCalledTimes(int index, int expected)
=> _counters[index].ShouldBe(expected, CalledTimesMessage());

public void ThenServicesShouldHaveBeenCalledTimes(params int[] expected)
{
for (int i = 0; i < expected.Length; i++)
{
_counters[i].ShouldBe(expected[i], CalledTimesMessage());
}
}

public static int Bottom(int totalRequests, int totalServices)
=> totalRequests / totalServices;
public static int Top(int totalRequests, int totalServices)
{
int bottom = Bottom(totalRequests, totalServices);
return totalRequests - (bottom * totalServices) + bottom;
}

public void ThenAllServicesCalledRealisticAmountOfTimes(int bottom, int top)
{
var customMessage = new StringBuilder()
.AppendLine($"{nameof(bottom)}: {bottom}")
.AppendLine($" {nameof(top)}: {top}")
.AppendLine($" All values are [{string.Join(',', _counters)}]")
.ToString();
int sum = 0, totalSum = _counters.Sum();

// Last offline services cannot be called at all, thus don't assert zero counters
for (int i = 0; i < _counters.Length && sum < totalSum; i++)
{
int actual = _counters[i];
actual.ShouldBeInRange(bottom, top, customMessage);
sum += actual;
}
}

public void ThenAllServicesCalledOptimisticAmountOfTimes(ILoadBalancerAnalyzer analyzer)
{
if (analyzer == null) return;
int bottom = analyzer.BottomOfConnections(),
top = analyzer.TopOfConnections();
ThenAllServicesCalledRealisticAmountOfTimes(bottom, top); // with unstable checkings
}

public void ThenServiceCountersShouldMatchLeasingCounters(ILoadBalancerAnalyzer analyzer, int[] ports, int totalRequests)
{
if (analyzer == null || ports == null)
return;

analyzer.ShouldNotBeNull().Analyze();
analyzer.Events.Count.ShouldBe(totalRequests, $"{nameof(ILoadBalancerAnalyzer.ServiceName)}: {analyzer.ServiceName}");

var leasingCounters = analyzer?.GetHostCounters() ?? new();
var sortedLeasingCountersByPort = ports.Select(port => leasingCounters.FirstOrDefault(kv => kv.Key.DownstreamPort == port).Value).ToArray();
for (int i = 0; i < ports.Length; i++)
{
var host = leasingCounters.Keys.FirstOrDefault(k => k.DownstreamPort == ports[i]);

// Leasing info/counters can be absent because of offline service instance with exact port in unstable scenario
if (host != null)
{
var customMessage = new StringBuilder()
.AppendLine($"{nameof(ILoadBalancerAnalyzer.ServiceName)}: {analyzer.ServiceName}")
.AppendLine($" Port: {ports[i]}")
.AppendLine($" Host: {host}")
.AppendLine($" Service counters: [{string.Join(',', _counters)}]")
.AppendLine($" Leasing counters: [{string.Join(',', sortedLeasingCountersByPort)}]") // should have order of _counters
.ToString();
int counter1 = _counters[i];
int counter2 = leasingCounters[host];
counter1.ShouldBe(counter2, customMessage);
}
}
}
}
18 changes: 18 additions & 0 deletions acceptance/LoadBalancer/ILoadBalancerAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Ocelot.LoadBalancer;
using Ocelot.Values;
using System.Collections.Concurrent;

namespace Ocelot.Discovery.Nacos.AcceptanceTests.LoadBalancer;

public interface ILoadBalancerAnalyzer
{
string ServiceName { get; }
string GenerationPrefix { get; }
ConcurrentBag<LeaseEventArgs> Events { get; }
object Analyze();
Dictionary<ServiceHostAndPort, int> GetHostCounters();
Dictionary<ServiceHostAndPort, int> ToHostCountersDictionary(IEnumerable<IGrouping<ServiceHostAndPort, LeaseEventArgs>> grouping);
bool HasManyServiceGenerations(int maxGeneration);
int BottomOfConnections();
int TopOfConnections();
}
Loading