using System.Collections.Concurrent;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Xunit;
using YarpGateway.Data;
using YarpGateway.DynamicProxy;
using YarpGateway.Services;
using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
namespace YarpGateway.Tests.Integration;
///
/// 网关配置热重载集成测试
///
[Collection("Integration Tests")]
public class ConfigReloadTests : IDisposable
{
private readonly TestFixture _fixture;
private readonly GatewayDbContext _dbContext;
public ConfigReloadTests(TestFixture fixture)
{
_fixture = fixture;
_dbContext = _fixture.CreateDbContext();
}
public void Dispose()
{
_dbContext.Dispose();
}
#region 配置重载测试
[Fact]
public async Task WhenConfigChanged_ReloadAsync_ShouldUpdateRouteCache()
{
// Arrange: 获取初始路由数量
var routeCache = _fixture.GetRouteCache();
var initialRoute = routeCache.GetRoute("test-tenant", "reload-test");
initialRoute.Should().BeNull(); // 初始时不存在
// 添加新路由
var newRoute = new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "test-tenant",
ServiceName = "reload-test",
ClusterId = "member-cluster",
Match = new GwRouteMatch { Path = "/api/reload-test/**" },
Priority = 1,
Status = 1,
IsGlobal = false,
CreatedTime = DateTime.UtcNow
};
_dbContext.GwTenantRoutes.Add(newRoute);
await _dbContext.SaveChangesAsync();
// Act: 重新加载配置
await routeCache.ReloadAsync();
// Assert: 应该能找到新路由
var reloadedRoute = routeCache.GetRoute("test-tenant", "reload-test");
reloadedRoute.Should().NotBeNull();
reloadedRoute!.ClusterId.Should().Be("member-cluster");
// 清理
_dbContext.GwTenantRoutes.Remove(newRoute);
await _dbContext.SaveChangesAsync();
}
[Fact]
public async Task WhenConfigChanged_ReloadAsync_ShouldUpdateProxyConfig()
{
// Arrange
var configProvider = _fixture.GetConfigProvider();
var initialConfig = configProvider.GetConfig();
var initialRouteCount = initialConfig.Routes.Count;
// 添加新路由
var newRoute = new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "",
ServiceName = "proxy-reload-test",
ClusterId = "member-cluster",
Match = new GwRouteMatch { Path = "/api/proxy-reload/**" },
Priority = 1,
Status = 1,
IsGlobal = true,
CreatedTime = DateTime.UtcNow
};
_dbContext.GwTenantRoutes.Add(newRoute);
await _dbContext.SaveChangesAsync();
// 重新加载 RouteCache
var routeCache = _fixture.GetRouteCache();
await routeCache.ReloadAsync();
// Act: 更新代理配置
configProvider.UpdateConfig();
// Assert
var newConfig = configProvider.GetConfig();
newConfig.Routes.Count.Should().BeGreaterThanOrEqualTo(initialRouteCount);
// 验证可以通过服务名找到路由
var route = newConfig.Routes.FirstOrDefault(r =>
r.Metadata?.ContainsKey("ServiceName") == true &&
r.Metadata["ServiceName"] == "proxy-reload-test");
route.Should().NotBeNull();
// 清理
_dbContext.GwTenantRoutes.Remove(newRoute);
await _dbContext.SaveChangesAsync();
}
[Fact]
public async Task WhenRouteDisabled_Reload_ShouldRemoveFromCache()
{
// Arrange: 添加并确认路由存在
var routeId = Guid.CreateVersion7().ToString("N");
var newRoute = new GwTenantRoute
{
Id = routeId,
TenantCode = "disable-test",
ServiceName = "disable-test-service",
ClusterId = "member-cluster",
Match = new GwRouteMatch { Path = "/api/disable/**" },
Priority = 1,
Status = 1,
IsGlobal = false,
CreatedTime = DateTime.UtcNow
};
_dbContext.GwTenantRoutes.Add(newRoute);
await _dbContext.SaveChangesAsync();
var routeCache = _fixture.GetRouteCache();
await routeCache.ReloadAsync();
// 确认路由存在
var routeBefore = routeCache.GetRoute("disable-test", "disable-test-service");
routeBefore.Should().NotBeNull();
// Act: 禁用路由
newRoute.Status = 0;
_dbContext.GwTenantRoutes.Update(newRoute);
await _dbContext.SaveChangesAsync();
await routeCache.ReloadAsync();
// Assert: 路由应该从缓存中移除
var routeAfter = routeCache.GetRoute("disable-test", "disable-test-service");
routeAfter.Should().BeNull();
// 清理
_dbContext.GwTenantRoutes.Remove(newRoute);
await _dbContext.SaveChangesAsync();
}
[Fact]
public async Task WhenRouteDeleted_Reload_ShouldRemoveFromCache()
{
// Arrange
var routeId = Guid.CreateVersion7().ToString("N");
var newRoute = new GwTenantRoute
{
Id = routeId,
TenantCode = "delete-test",
ServiceName = "delete-test-service",
ClusterId = "member-cluster",
Match = new GwRouteMatch { Path = "/api/delete/**" },
Priority = 1,
Status = 1,
IsGlobal = false,
IsDeleted = false,
CreatedTime = DateTime.UtcNow
};
_dbContext.GwTenantRoutes.Add(newRoute);
await _dbContext.SaveChangesAsync();
var routeCache = _fixture.GetRouteCache();
await routeCache.ReloadAsync();
// 确认路由存在
var routeBefore = routeCache.GetRoute("delete-test", "delete-test-service");
routeBefore.Should().NotBeNull();
// Act: 软删除路由
newRoute.IsDeleted = true;
_dbContext.GwTenantRoutes.Update(newRoute);
await _dbContext.SaveChangesAsync();
await routeCache.ReloadAsync();
// Assert: 路由应该从缓存中移除
var routeAfter = routeCache.GetRoute("delete-test", "delete-test-service");
routeAfter.Should().BeNull();
// 清理
_dbContext.GwTenantRoutes.Remove(newRoute);
await _dbContext.SaveChangesAsync();
}
[Fact]
public async Task WhenClusterChanged_Reload_ShouldUpdateClusterConfig()
{
// Arrange
var configProvider = _fixture.GetConfigProvider();
// 添加新集群
var newCluster = new GwCluster
{
Id = Guid.CreateVersion7().ToString("N"),
ClusterId = "reload-test-cluster",
Name = "Reload Test Cluster",
Status = 1,
CreatedTime = DateTime.UtcNow,
LoadBalancingPolicy = GwLoadBalancingPolicy.RoundRobin,
Destinations = new List
{
new()
{
DestinationId = "dest-1",
Address = "http://reload-test:8080",
Weight = 1,
Status = 1
}
}
};
_dbContext.GwClusters.Add(newCluster);
await _dbContext.SaveChangesAsync();
// Act
await _fixture.ReloadConfigurationAsync();
// Assert
var config = configProvider.GetConfig();
var cluster = config.Clusters.FirstOrDefault(c => c.ClusterId == "reload-test-cluster");
cluster.Should().NotBeNull();
cluster!.Destinations.Should().ContainKey("dest-1");
// 清理
_dbContext.GwClusters.Remove(newCluster);
await _dbContext.SaveChangesAsync();
}
[Fact]
public async Task WhenDestinationAdded_Reload_ShouldIncludeNewDestination()
{
// Arrange: 获取现有集群
var cluster = await _dbContext.GwClusters
.Include(c => c.Destinations)
.FirstOrDefaultAsync(c => c.ClusterId == "member-cluster");
cluster.Should().NotBeNull();
var initialDestCount = cluster!.Destinations.Count;
// 添加新目标
var newDest = new GwDestination
{
DestinationId = "new-dest",
Address = "http://new-destination:8080",
Weight = 1,
Status = 1
};
cluster.Destinations.Add(newDest);
await _dbContext.SaveChangesAsync();
// Act
await _fixture.ReloadConfigurationAsync();
// Assert
var configProvider = _fixture.GetConfigProvider();
var config = configProvider.GetConfig();
var updatedCluster = config.Clusters.FirstOrDefault(c => c.ClusterId == "member-cluster");
updatedCluster.Should().NotBeNull();
updatedCluster!.Destinations.Should().ContainKey("new-dest");
// 清理
cluster.Destinations.Remove(newDest);
await _dbContext.SaveChangesAsync();
}
#endregion
#region 并发重载测试
[Fact]
public async Task ConcurrentReload_ShouldBeThreadSafe()
{
// Arrange
var routeCache = _fixture.GetRouteCache();
var configProvider = _fixture.GetConfigProvider();
// Act: 并发执行多次重载
var tasks = Enumerable.Range(0, 10)
.Select(_ => Task.Run(async () =>
{
await routeCache.ReloadAsync();
configProvider.UpdateConfig();
}))
.ToList();
// 不应该抛出异常
await Task.WhenAll(tasks);
// Assert: 配置应该保持一致
var config = configProvider.GetConfig();
config.Should().NotBeNull();
config.Routes.Should().NotBeNull();
config.Clusters.Should().NotBeNull();
}
#endregion
#region 配置变更通知测试(模拟)
[Fact]
public async Task SimulateNotify_ShouldTriggerReload()
{
// Arrange
var routeCache = _fixture.GetRouteCache();
var configProvider = _fixture.GetConfigProvider();
// 添加新路由
var newRoute = new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "",
ServiceName = "notify-test",
ClusterId = "member-cluster",
Match = new GwRouteMatch { Path = "/api/notify/**" },
Priority = 1,
Status = 1,
IsGlobal = true,
CreatedTime = DateTime.UtcNow
};
_dbContext.GwTenantRoutes.Add(newRoute);
await _dbContext.SaveChangesAsync();
// Act: 模拟接收到 NOTIFY 事件后手动触发重载
// 在真实环境中,这是由 PgSqlConfigChangeListener 自动处理的
await routeCache.ReloadAsync();
configProvider.UpdateConfig();
// Assert
var route = routeCache.GetRoute("any-tenant", "notify-test");
route.Should().NotBeNull();
route!.ClusterId.Should().Be("test-cluster");
// 清理
_dbContext.GwTenantRoutes.Remove(newRoute);
await _dbContext.SaveChangesAsync();
}
[Fact]
public async Task ConfigChangeToken_ShouldSignalChange()
{
// Arrange
var configProvider = _fixture.GetConfigProvider();
var config = configProvider.GetConfig();
// 获取变更令牌
var changeToken = config.ChangeToken;
var changeDetected = false;
// 注册变更回调
using var registration = changeToken.RegisterChangeCallback(_ =>
{
changeDetected = true;
}, null);
// Act: 添加新路由并更新配置
var newRoute = new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "",
ServiceName = "token-test",
ClusterId = "member-cluster",
Match = new GwRouteMatch { Path = "/api/token/**" },
Priority = 1,
Status = 1,
IsGlobal = true,
CreatedTime = DateTime.UtcNow
};
_dbContext.GwTenantRoutes.Add(newRoute);
await _dbContext.SaveChangesAsync();
await _fixture.ReloadConfigurationAsync();
// 获取新配置
var newConfig = configProvider.GetConfig();
// Assert: 新配置的 ChangeToken 应该与老配置不同
newConfig.ChangeToken.Should().NotBeSameAs(changeToken);
// 清理
_dbContext.GwTenantRoutes.Remove(newRoute);
await _dbContext.SaveChangesAsync();
}
#endregion
#region 性能测试
[Fact]
public async Task ReloadPerformance_ShouldBeFast()
{
// Arrange
var routeCache = _fixture.GetRouteCache();
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
// Act: 执行多次重载
for (int i = 0; i < 10; i++)
{
await routeCache.ReloadAsync();
}
stopwatch.Stop();
// Assert: 10 次重载应该在合理时间内完成
stopwatch.ElapsedMilliseconds.Should().BeLessThan(5000);
}
[Fact]
public async Task LargeConfigReload_ShouldHandleGracefully()
{
// Arrange: 批量添加路由
var routes = new List();
for (int i = 0; i < 100; i++)
{
routes.Add(new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = $"perf-tenant-{i % 10}",
ServiceName = $"perf-service-{i}",
ClusterId = "member-cluster",
Match = new GwRouteMatch { Path = $"/api/perf-{i}/**" },
Priority = 1,
Status = 1,
IsGlobal = i % 10 == 0,
CreatedTime = DateTime.UtcNow
});
}
_dbContext.GwTenantRoutes.AddRange(routes);
await _dbContext.SaveChangesAsync();
var routeCache = _fixture.GetRouteCache();
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
// Act
await routeCache.ReloadAsync();
stopwatch.Stop();
// Assert: 100 条路由的重载应该在合理时间内完成
stopwatch.ElapsedMilliseconds.Should().BeLessThan(5000);
// 验证路由已加载
var route = routeCache.GetRoute("perf-tenant-0", "perf-service-0");
route.Should().NotBeNull();
// 清理
_dbContext.GwTenantRoutes.RemoveRange(routes);
await _dbContext.SaveChangesAsync();
}
#endregion
}