feat(rate-limiting): Implement core rate limiting functionality with configuration, decision-making, metrics, middleware, and service registration
- Add RateLimitConfig for configuration management with YAML binding support. - Introduce RateLimitDecision to encapsulate the result of rate limit checks. - Implement RateLimitMetrics for OpenTelemetry metrics tracking. - Create RateLimitMiddleware for enforcing rate limits on incoming requests. - Develop RateLimitService to orchestrate instance and environment rate limit checks. - Add RateLimitServiceCollectionExtensions for dependency injection registration.
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_3500_0001_0001
|
||||
// Task: SDIFF-MASTER-0007 - Performance benchmark suite
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Columns;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Exporters;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using BenchmarkDotNet.Loggers;
|
||||
using BenchmarkDotNet.Running;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.SmartDiff.Tests.Benchmarks;
|
||||
|
||||
/// <summary>
|
||||
/// BenchmarkDotNet performance benchmarks for Smart-Diff operations.
|
||||
/// Run with: dotnet run -c Release --project StellaOps.Scanner.SmartDiff.Tests.csproj -- --filter *SmartDiff*
|
||||
/// </summary>
|
||||
[Config(typeof(SmartDiffBenchmarkConfig))]
|
||||
[MemoryDiagnoser]
|
||||
[RankColumn]
|
||||
public class SmartDiffPerformanceBenchmarks
|
||||
{
|
||||
private ScanData _smallBaseline = null!;
|
||||
private ScanData _smallCurrent = null!;
|
||||
private ScanData _mediumBaseline = null!;
|
||||
private ScanData _mediumCurrent = null!;
|
||||
private ScanData _largeBaseline = null!;
|
||||
private ScanData _largeCurrent = null!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
// Small: 50 packages, 10 vulnerabilities
|
||||
_smallBaseline = GenerateScanData(packageCount: 50, vulnCount: 10);
|
||||
_smallCurrent = GenerateScanData(packageCount: 55, vulnCount: 12, deltaPercent: 0.2);
|
||||
|
||||
// Medium: 500 packages, 100 vulnerabilities
|
||||
_mediumBaseline = GenerateScanData(packageCount: 500, vulnCount: 100);
|
||||
_mediumCurrent = GenerateScanData(packageCount: 520, vulnCount: 110, deltaPercent: 0.15);
|
||||
|
||||
// Large: 5000 packages, 1000 vulnerabilities
|
||||
_largeBaseline = GenerateScanData(packageCount: 5000, vulnCount: 1000);
|
||||
_largeCurrent = GenerateScanData(packageCount: 5100, vulnCount: 1050, deltaPercent: 0.10);
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public DiffResult SmallScan_ComputeDiff()
|
||||
{
|
||||
return ComputeDiff(_smallBaseline, _smallCurrent);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public DiffResult MediumScan_ComputeDiff()
|
||||
{
|
||||
return ComputeDiff(_mediumBaseline, _mediumCurrent);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public DiffResult LargeScan_ComputeDiff()
|
||||
{
|
||||
return ComputeDiff(_largeBaseline, _largeCurrent);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public string SmallScan_GenerateSarif()
|
||||
{
|
||||
var diff = ComputeDiff(_smallBaseline, _smallCurrent);
|
||||
return GenerateSarif(diff);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public string MediumScan_GenerateSarif()
|
||||
{
|
||||
var diff = ComputeDiff(_mediumBaseline, _mediumCurrent);
|
||||
return GenerateSarif(diff);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public string LargeScan_GenerateSarif()
|
||||
{
|
||||
var diff = ComputeDiff(_largeBaseline, _largeCurrent);
|
||||
return GenerateSarif(diff);
|
||||
}
|
||||
|
||||
#region Benchmark Helpers
|
||||
|
||||
private static ScanData GenerateScanData(int packageCount, int vulnCount, double deltaPercent = 0)
|
||||
{
|
||||
var random = new Random(42); // Fixed seed for reproducibility
|
||||
var packages = new List<PackageInfo>();
|
||||
var vulnerabilities = new List<VulnInfo>();
|
||||
|
||||
for (int i = 0; i < packageCount; i++)
|
||||
{
|
||||
packages.Add(new PackageInfo
|
||||
{
|
||||
Name = $"package-{i:D5}",
|
||||
Version = $"1.{random.Next(0, 10)}.{random.Next(0, 100)}",
|
||||
Ecosystem = random.Next(0, 3) switch { 0 => "npm", 1 => "nuget", _ => "pypi" }
|
||||
});
|
||||
}
|
||||
|
||||
for (int i = 0; i < vulnCount; i++)
|
||||
{
|
||||
var pkg = packages[random.Next(0, packages.Count)];
|
||||
vulnerabilities.Add(new VulnInfo
|
||||
{
|
||||
CveId = $"CVE-2024-{10000 + i}",
|
||||
Package = pkg.Name,
|
||||
Version = pkg.Version,
|
||||
Severity = random.Next(0, 4) switch { 0 => "LOW", 1 => "MEDIUM", 2 => "HIGH", _ => "CRITICAL" },
|
||||
IsReachable = random.NextDouble() > 0.6,
|
||||
ReachabilityTier = random.Next(0, 3) switch { 0 => "imported", 1 => "called", _ => "executed" }
|
||||
});
|
||||
}
|
||||
|
||||
// Apply delta for current scans
|
||||
if (deltaPercent > 0)
|
||||
{
|
||||
int vulnsToAdd = (int)(vulnCount * deltaPercent);
|
||||
for (int i = 0; i < vulnsToAdd; i++)
|
||||
{
|
||||
var pkg = packages[random.Next(0, packages.Count)];
|
||||
vulnerabilities.Add(new VulnInfo
|
||||
{
|
||||
CveId = $"CVE-2024-{20000 + i}",
|
||||
Package = pkg.Name,
|
||||
Version = pkg.Version,
|
||||
Severity = "HIGH",
|
||||
IsReachable = true,
|
||||
ReachabilityTier = "executed"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new ScanData { Packages = packages, Vulnerabilities = vulnerabilities };
|
||||
}
|
||||
|
||||
private static DiffResult ComputeDiff(ScanData baseline, ScanData current)
|
||||
{
|
||||
var baselineSet = baseline.Vulnerabilities.ToHashSet(new VulnComparer());
|
||||
var currentSet = current.Vulnerabilities.ToHashSet(new VulnComparer());
|
||||
|
||||
var added = current.Vulnerabilities.Where(v => !baselineSet.Contains(v)).ToList();
|
||||
var removed = baseline.Vulnerabilities.Where(v => !currentSet.Contains(v)).ToList();
|
||||
|
||||
// Detect reachability flips
|
||||
var baselineDict = baseline.Vulnerabilities.ToDictionary(v => v.CveId);
|
||||
var reachabilityFlips = new List<VulnInfo>();
|
||||
foreach (var curr in current.Vulnerabilities)
|
||||
{
|
||||
if (baselineDict.TryGetValue(curr.CveId, out var prev) && prev.IsReachable != curr.IsReachable)
|
||||
{
|
||||
reachabilityFlips.Add(curr);
|
||||
}
|
||||
}
|
||||
|
||||
return new DiffResult
|
||||
{
|
||||
Added = added,
|
||||
Removed = removed,
|
||||
ReachabilityFlips = reachabilityFlips,
|
||||
TotalBaselineVulns = baseline.Vulnerabilities.Count,
|
||||
TotalCurrentVulns = current.Vulnerabilities.Count
|
||||
};
|
||||
}
|
||||
|
||||
private static string GenerateSarif(DiffResult diff)
|
||||
{
|
||||
var sarif = new
|
||||
{
|
||||
version = "2.1.0",
|
||||
schema = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
||||
runs = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
tool = new
|
||||
{
|
||||
driver = new
|
||||
{
|
||||
name = "StellaOps Smart-Diff",
|
||||
version = "1.0.0",
|
||||
informationUri = "https://stellaops.io"
|
||||
}
|
||||
},
|
||||
results = diff.Added.Select(v => new
|
||||
{
|
||||
ruleId = v.CveId,
|
||||
level = v.Severity == "CRITICAL" || v.Severity == "HIGH" ? "error" : "warning",
|
||||
message = new { text = $"New vulnerability {v.CveId} in {v.Package}@{v.Version}" },
|
||||
locations = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
physicalLocation = new
|
||||
{
|
||||
artifactLocation = new { uri = $"pkg:{v.Package}@{v.Version}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}).ToArray()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(sarif, new JsonSerializerOptions { WriteIndented = false });
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performance threshold tests that fail CI if benchmarks regress.
|
||||
/// </summary>
|
||||
public sealed class SmartDiffPerformanceTests
|
||||
{
|
||||
[Fact]
|
||||
public void SmallScan_ShouldCompleteWithin50ms()
|
||||
{
|
||||
// Arrange
|
||||
var baseline = GenerateTestData(50, 10);
|
||||
var current = GenerateTestData(55, 12);
|
||||
|
||||
// Act
|
||||
var sw = Stopwatch.StartNew();
|
||||
var result = ComputeDiff(baseline, current);
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
sw.ElapsedMilliseconds.Should().BeLessThan(50, "Small scan diff should complete within 50ms");
|
||||
result.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MediumScan_ShouldCompleteWithin200ms()
|
||||
{
|
||||
// Arrange
|
||||
var baseline = GenerateTestData(500, 100);
|
||||
var current = GenerateTestData(520, 110);
|
||||
|
||||
// Act
|
||||
var sw = Stopwatch.StartNew();
|
||||
var result = ComputeDiff(baseline, current);
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
sw.ElapsedMilliseconds.Should().BeLessThan(200, "Medium scan diff should complete within 200ms");
|
||||
result.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeScan_ShouldCompleteWithin2000ms()
|
||||
{
|
||||
// Arrange
|
||||
var baseline = GenerateTestData(5000, 1000);
|
||||
var current = GenerateTestData(5100, 1050);
|
||||
|
||||
// Act
|
||||
var sw = Stopwatch.StartNew();
|
||||
var result = ComputeDiff(baseline, current);
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
sw.ElapsedMilliseconds.Should().BeLessThan(2000, "Large scan diff should complete within 2 seconds");
|
||||
result.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SarifGeneration_ShouldCompleteWithin100ms_ForSmallDiff()
|
||||
{
|
||||
// Arrange
|
||||
var baseline = GenerateTestData(50, 10);
|
||||
var current = GenerateTestData(55, 15);
|
||||
var diff = ComputeDiff(baseline, current);
|
||||
|
||||
// Act
|
||||
var sw = Stopwatch.StartNew();
|
||||
var sarif = GenerateSarif(diff);
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
sw.ElapsedMilliseconds.Should().BeLessThan(100, "SARIF generation should complete within 100ms");
|
||||
sarif.Should().Contain("2.1.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MemoryUsage_ShouldBeReasonable_ForLargeScan()
|
||||
{
|
||||
// Arrange
|
||||
var baseline = GenerateTestData(5000, 1000);
|
||||
var current = GenerateTestData(5100, 1050);
|
||||
|
||||
var memBefore = GC.GetTotalMemory(forceFullCollection: true);
|
||||
|
||||
// Act
|
||||
var result = ComputeDiff(baseline, current);
|
||||
var sarif = GenerateSarif(result);
|
||||
|
||||
var memAfter = GC.GetTotalMemory(forceFullCollection: false);
|
||||
var memUsedMB = (memAfter - memBefore) / (1024.0 * 1024.0);
|
||||
|
||||
// Assert
|
||||
memUsedMB.Should().BeLessThan(100, "Large scan diff should use less than 100MB of memory");
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
private static ScanData GenerateTestData(int packageCount, int vulnCount)
|
||||
{
|
||||
var random = new Random(42);
|
||||
var packages = Enumerable.Range(0, packageCount)
|
||||
.Select(i => new PackageInfo { Name = $"pkg-{i}", Version = "1.0.0", Ecosystem = "npm" })
|
||||
.ToList();
|
||||
|
||||
var vulns = Enumerable.Range(0, vulnCount)
|
||||
.Select(i => new VulnInfo
|
||||
{
|
||||
CveId = $"CVE-2024-{i}",
|
||||
Package = packages[random.Next(packages.Count)].Name,
|
||||
Version = "1.0.0",
|
||||
Severity = "HIGH",
|
||||
IsReachable = random.NextDouble() > 0.5,
|
||||
ReachabilityTier = "executed"
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return new ScanData { Packages = packages, Vulnerabilities = vulns };
|
||||
}
|
||||
|
||||
private static DiffResult ComputeDiff(ScanData baseline, ScanData current)
|
||||
{
|
||||
var baselineSet = baseline.Vulnerabilities.Select(v => v.CveId).ToHashSet();
|
||||
var currentSet = current.Vulnerabilities.Select(v => v.CveId).ToHashSet();
|
||||
|
||||
return new DiffResult
|
||||
{
|
||||
Added = current.Vulnerabilities.Where(v => !baselineSet.Contains(v.CveId)).ToList(),
|
||||
Removed = baseline.Vulnerabilities.Where(v => !currentSet.Contains(v.CveId)).ToList(),
|
||||
ReachabilityFlips = new List<VulnInfo>(),
|
||||
TotalBaselineVulns = baseline.Vulnerabilities.Count,
|
||||
TotalCurrentVulns = current.Vulnerabilities.Count
|
||||
};
|
||||
}
|
||||
|
||||
private static string GenerateSarif(DiffResult diff)
|
||||
{
|
||||
return JsonSerializer.Serialize(new
|
||||
{
|
||||
version = "2.1.0",
|
||||
runs = new[] { new { results = diff.Added.Count } }
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#region Benchmark Config
|
||||
|
||||
public sealed class SmartDiffBenchmarkConfig : ManualConfig
|
||||
{
|
||||
public SmartDiffBenchmarkConfig()
|
||||
{
|
||||
AddJob(Job.ShortRun
|
||||
.WithWarmupCount(3)
|
||||
.WithIterationCount(5));
|
||||
|
||||
AddLogger(ConsoleLogger.Default);
|
||||
AddExporter(MarkdownExporter.GitHub);
|
||||
AddExporter(HtmlExporter.Default);
|
||||
AddColumnProvider(DefaultColumnProviders.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Models
|
||||
|
||||
public sealed class ScanData
|
||||
{
|
||||
public List<PackageInfo> Packages { get; set; } = new();
|
||||
public List<VulnInfo> Vulnerabilities { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class PackageInfo
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string Version { get; set; } = "";
|
||||
public string Ecosystem { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class VulnInfo
|
||||
{
|
||||
public string CveId { get; set; } = "";
|
||||
public string Package { get; set; } = "";
|
||||
public string Version { get; set; } = "";
|
||||
public string Severity { get; set; } = "";
|
||||
public bool IsReachable { get; set; }
|
||||
public string ReachabilityTier { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class DiffResult
|
||||
{
|
||||
public List<VulnInfo> Added { get; set; } = new();
|
||||
public List<VulnInfo> Removed { get; set; } = new();
|
||||
public List<VulnInfo> ReachabilityFlips { get; set; } = new();
|
||||
public int TotalBaselineVulns { get; set; }
|
||||
public int TotalCurrentVulns { get; set; }
|
||||
}
|
||||
|
||||
public sealed class VulnComparer : IEqualityComparer<VulnInfo>
|
||||
{
|
||||
public bool Equals(VulnInfo? x, VulnInfo? y)
|
||||
{
|
||||
if (x is null || y is null) return false;
|
||||
return x.CveId == y.CveId && x.Package == y.Package && x.Version == y.Version;
|
||||
}
|
||||
|
||||
public int GetHashCode(VulnInfo obj)
|
||||
{
|
||||
return HashCode.Combine(obj.CveId, obj.Package, obj.Version);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
Reference in New Issue
Block a user