feat: add service export latency tracking with p50/p90/p99 (Gap 9.1)
Add ServiceLatencyTracker with sorted-sample histogram, percentile getters (p50/p90/p99), average/min/max, reset, and immutable snapshot. Wire LatencyTracker and RecordServiceLatency onto Account. Cover with 11 xUnit tests.
This commit is contained in:
168
tests/NATS.Server.Tests/Auth/ServiceLatencyTrackerTests.cs
Normal file
168
tests/NATS.Server.Tests/Auth/ServiceLatencyTrackerTests.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
// Tests for service export latency tracker with p50/p90/p99 percentile histogram.
|
||||
// Go reference: accounts_test.go TestServiceLatency, serviceExportLatencyStats.
|
||||
|
||||
using NATS.Server.Auth;
|
||||
|
||||
namespace NATS.Server.Tests.Auth;
|
||||
|
||||
public class ServiceLatencyTrackerTests
|
||||
{
|
||||
[Fact]
|
||||
public void RecordLatency_IncrementsTotalRequests()
|
||||
{
|
||||
var tracker = new ServiceLatencyTracker();
|
||||
|
||||
tracker.RecordLatency(10.0);
|
||||
tracker.RecordLatency(20.0);
|
||||
tracker.RecordLatency(30.0);
|
||||
|
||||
tracker.TotalRequests.ShouldBe(3L);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetP50_ReturnsMedian()
|
||||
{
|
||||
var tracker = new ServiceLatencyTracker();
|
||||
|
||||
foreach (var v in new double[] { 1, 2, 3, 4, 5 })
|
||||
tracker.RecordLatency(v);
|
||||
|
||||
// Sorted: [1, 2, 3, 4, 5], index = (int)(0.50 * 4) = 2 → value 3
|
||||
tracker.GetP50().ShouldBe(3.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetP90_ReturnsHighPercentile()
|
||||
{
|
||||
var tracker = new ServiceLatencyTracker();
|
||||
|
||||
for (var i = 1; i <= 100; i++)
|
||||
tracker.RecordLatency(i);
|
||||
|
||||
// Sorted [1..100], index = (int)(0.90 * 99) = (int)89.1 = 89 → value 90
|
||||
tracker.GetP90().ShouldBe(90.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetP99_ReturnsTopPercentile()
|
||||
{
|
||||
var tracker = new ServiceLatencyTracker();
|
||||
|
||||
for (var i = 1; i <= 100; i++)
|
||||
tracker.RecordLatency(i);
|
||||
|
||||
// Sorted [1..100], index = (int)(0.99 * 99) = (int)98.01 = 98 → value 99
|
||||
tracker.GetP99().ShouldBe(99.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AverageLatencyMs_CalculatesCorrectly()
|
||||
{
|
||||
var tracker = new ServiceLatencyTracker();
|
||||
|
||||
tracker.RecordLatency(10.0);
|
||||
tracker.RecordLatency(20.0);
|
||||
tracker.RecordLatency(30.0);
|
||||
|
||||
tracker.AverageLatencyMs.ShouldBe(20.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinLatencyMs_ReturnsMinimum()
|
||||
{
|
||||
var tracker = new ServiceLatencyTracker();
|
||||
|
||||
tracker.RecordLatency(15.0);
|
||||
tracker.RecordLatency(5.0);
|
||||
tracker.RecordLatency(10.0);
|
||||
|
||||
tracker.MinLatencyMs.ShouldBe(5.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxLatencyMs_ReturnsMaximum()
|
||||
{
|
||||
var tracker = new ServiceLatencyTracker();
|
||||
|
||||
tracker.RecordLatency(5.0);
|
||||
tracker.RecordLatency(15.0);
|
||||
tracker.RecordLatency(10.0);
|
||||
|
||||
tracker.MaxLatencyMs.ShouldBe(15.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsSamples()
|
||||
{
|
||||
var tracker = new ServiceLatencyTracker();
|
||||
|
||||
tracker.RecordLatency(10.0);
|
||||
tracker.RecordLatency(20.0);
|
||||
tracker.SampleCount.ShouldBe(2);
|
||||
tracker.TotalRequests.ShouldBe(2L);
|
||||
|
||||
tracker.Reset();
|
||||
|
||||
tracker.SampleCount.ShouldBe(0);
|
||||
tracker.TotalRequests.ShouldBe(0L);
|
||||
tracker.AverageLatencyMs.ShouldBe(0.0);
|
||||
tracker.MinLatencyMs.ShouldBe(0.0);
|
||||
tracker.MaxLatencyMs.ShouldBe(0.0);
|
||||
tracker.GetP50().ShouldBe(0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSnapshot_ReturnsImmutableSnapshot()
|
||||
{
|
||||
var tracker = new ServiceLatencyTracker();
|
||||
|
||||
tracker.RecordLatency(10.0);
|
||||
tracker.RecordLatency(20.0);
|
||||
tracker.RecordLatency(30.0);
|
||||
|
||||
var snapshot = tracker.GetSnapshot();
|
||||
|
||||
snapshot.TotalRequests.ShouldBe(3L);
|
||||
snapshot.SampleCount.ShouldBe(3);
|
||||
snapshot.AverageMs.ShouldBe(20.0);
|
||||
snapshot.MinMs.ShouldBe(10.0);
|
||||
snapshot.MaxMs.ShouldBe(30.0);
|
||||
// P50 of [10, 20, 30]: index = (int)(0.50 * 2) = 1 → 20
|
||||
snapshot.P50Ms.ShouldBe(20.0);
|
||||
|
||||
// Mutating tracker after snapshot does not change the snapshot
|
||||
tracker.RecordLatency(1000.0);
|
||||
snapshot.MaxMs.ShouldBe(30.0);
|
||||
snapshot.SampleCount.ShouldBe(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSamples_EvictsOldest()
|
||||
{
|
||||
var tracker = new ServiceLatencyTracker(maxSamples: 5);
|
||||
|
||||
for (var i = 1; i <= 10; i++)
|
||||
tracker.RecordLatency(i);
|
||||
|
||||
// Only the last 5 samples should remain (6, 7, 8, 9, 10)
|
||||
tracker.SampleCount.ShouldBe(5);
|
||||
// TotalRequests counts all recorded calls, not just retained ones
|
||||
tracker.TotalRequests.ShouldBe(10L);
|
||||
// Minimum of retained samples is 6
|
||||
tracker.MinLatencyMs.ShouldBe(6.0);
|
||||
// Maximum of retained samples is 10
|
||||
tracker.MaxLatencyMs.ShouldBe(10.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Account_RecordServiceLatency_DelegatesToTracker()
|
||||
{
|
||||
var account = new Account("test");
|
||||
|
||||
account.RecordServiceLatency(50.0);
|
||||
account.RecordServiceLatency(100.0);
|
||||
|
||||
account.LatencyTracker.TotalRequests.ShouldBe(2L);
|
||||
account.LatencyTracker.AverageLatencyMs.ShouldBe(75.0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user