feat(routes): add pool accounting per account and S2 compression codec (D2+D3)
D2: Add FNV-1a-based ComputeRoutePoolIdx to RouteManager matching Go's route.go:533-545, with PoolIndex on RouteConnection and account-aware ForwardRoutedMessageAsync that routes to the correct pool connection. D3: Replace DeflateStream with IronSnappy in RouteCompressionCodec, add RouteCompressionLevel enum, NegotiateCompression, and IsCompressed detection. 17 new tests (6 pool + 11 compression), all passing.
This commit is contained in:
136
tests/NATS.Server.Tests/Routes/RouteS2CompressionTests.cs
Normal file
136
tests/NATS.Server.Tests/Routes/RouteS2CompressionTests.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
// Reference: golang/nats-server/server/route.go — S2/Snappy compression for routes
|
||||
// Tests for RouteCompressionCodec: compression, decompression, negotiation, detection.
|
||||
|
||||
using System.Text;
|
||||
using NATS.Server.Routes;
|
||||
|
||||
namespace NATS.Server.Tests.Routes;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for route S2/Snappy compression codec, matching Go's route compression
|
||||
/// behavior using IronSnappy.
|
||||
/// </summary>
|
||||
public class RouteS2CompressionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Compress_Fast_ProducesValidOutput()
|
||||
{
|
||||
var data = Encoding.UTF8.GetBytes("NATS route compression test payload");
|
||||
var compressed = RouteCompressionCodec.Compress(data, RouteCompressionLevel.Fast);
|
||||
|
||||
compressed.ShouldNotBeNull();
|
||||
compressed.Length.ShouldBeGreaterThan(0);
|
||||
|
||||
// Compressed output should be decompressible
|
||||
var decompressed = RouteCompressionCodec.Decompress(compressed);
|
||||
decompressed.ShouldBe(data);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compress_Decompress_RoundTrips()
|
||||
{
|
||||
var original = Encoding.UTF8.GetBytes("Hello NATS! This is a test of round-trip compression.");
|
||||
|
||||
foreach (var level in new[] { RouteCompressionLevel.Fast, RouteCompressionLevel.Better, RouteCompressionLevel.Best })
|
||||
{
|
||||
var compressed = RouteCompressionCodec.Compress(original, level);
|
||||
var restored = RouteCompressionCodec.Decompress(compressed);
|
||||
restored.ShouldBe(original, $"Round-trip failed for level {level}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compress_EmptyData_ReturnsEmpty()
|
||||
{
|
||||
var result = RouteCompressionCodec.Compress(ReadOnlySpan<byte>.Empty, RouteCompressionLevel.Fast);
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compress_Off_ReturnsOriginal()
|
||||
{
|
||||
var data = Encoding.UTF8.GetBytes("uncompressed payload");
|
||||
var result = RouteCompressionCodec.Compress(data, RouteCompressionLevel.Off);
|
||||
|
||||
result.ShouldBe(data);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decompress_CorruptedData_Throws()
|
||||
{
|
||||
var garbage = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04 };
|
||||
|
||||
Should.Throw<Exception>(() => RouteCompressionCodec.Decompress(garbage));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NegotiateCompression_BothOff_ReturnsOff()
|
||||
{
|
||||
var result = RouteCompressionCodec.NegotiateCompression("off", "off");
|
||||
result.ShouldBe(RouteCompressionLevel.Off);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NegotiateCompression_OneFast_ReturnsFast()
|
||||
{
|
||||
// When both are fast, result is fast
|
||||
var result = RouteCompressionCodec.NegotiateCompression("fast", "fast");
|
||||
result.ShouldBe(RouteCompressionLevel.Fast);
|
||||
|
||||
// When one is off, result is off (off wins)
|
||||
var result2 = RouteCompressionCodec.NegotiateCompression("fast", "off");
|
||||
result2.ShouldBe(RouteCompressionLevel.Off);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NegotiateCompression_MismatchLevels_ReturnsMinimum()
|
||||
{
|
||||
// fast (1) vs best (3) => fast (minimum)
|
||||
var result = RouteCompressionCodec.NegotiateCompression("fast", "best");
|
||||
result.ShouldBe(RouteCompressionLevel.Fast);
|
||||
|
||||
// better (2) vs best (3) => better (minimum)
|
||||
var result2 = RouteCompressionCodec.NegotiateCompression("better", "best");
|
||||
result2.ShouldBe(RouteCompressionLevel.Better);
|
||||
|
||||
// fast (1) vs better (2) => fast (minimum)
|
||||
var result3 = RouteCompressionCodec.NegotiateCompression("fast", "better");
|
||||
result3.ShouldBe(RouteCompressionLevel.Fast);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsCompressed_ValidSnappy_ReturnsTrue()
|
||||
{
|
||||
var data = Encoding.UTF8.GetBytes("This is test data for Snappy compression detection");
|
||||
var compressed = RouteCompressionCodec.Compress(data, RouteCompressionLevel.Fast);
|
||||
|
||||
RouteCompressionCodec.IsCompressed(compressed).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsCompressed_PlainText_ReturnsFalse()
|
||||
{
|
||||
var plainText = Encoding.UTF8.GetBytes("PUB test.subject 5\r\nhello\r\n");
|
||||
|
||||
RouteCompressionCodec.IsCompressed(plainText).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_LargePayload_Compresses()
|
||||
{
|
||||
// 10KB payload of repeated data should compress well
|
||||
var largePayload = new byte[10240];
|
||||
var pattern = Encoding.UTF8.GetBytes("NATS route payload ");
|
||||
for (var i = 0; i < largePayload.Length; i++)
|
||||
largePayload[i] = pattern[i % pattern.Length];
|
||||
|
||||
var compressed = RouteCompressionCodec.Compress(largePayload, RouteCompressionLevel.Fast);
|
||||
|
||||
// Compressed should be smaller than original for repetitive data
|
||||
compressed.Length.ShouldBeLessThan(largePayload.Length);
|
||||
|
||||
// Round-trip should restore original
|
||||
var restored = RouteCompressionCodec.Decompress(compressed);
|
||||
restored.ShouldBe(largePayload);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user