fix: prevent QUIC heartbeat stalls and isolate TCP retransmission counters [CI] [SKIP TESTS]
This commit is contained in:
1 parent
0066a947da
commit
0fd38b2367
14 files changed
+299
-50
No files matched your search
@@ -203,7 +203,7 @@ public sealed class MeasurementTests
|
||||
var builder = new StreamBuilder(spec);
|
||||
foreach ((long ts, long side, long retrans) in rows)
|
||||
{
|
||||
builder.Add(ts, side, 40_000, 5_000, 10, retrans, 100, 1_000);
|
||||
builder.Add(ts, side, 40_000, 5_000, 10, retrans, 100, 1_000, null);
|
||||
}
|
||||
|
||||
return builder.Build();
|
||||
@@ -216,8 +216,8 @@ public sealed class MeasurementTests
|
||||
Assert.Equal(info.Columns.Count, info.ColumnsOf(Side.Agent).Count); // each side owns every column
|
||||
var agent = new RowStore(info, Side.Agent, 64);
|
||||
var recorder = new Source { Stores = { ["tcp_info"] = agent } };
|
||||
agent.SetRow(agent.Append(), [500, 1, 40_000, 5_000, 10, 7, 100, 1_000]);
|
||||
agent.SetRow(agent.Append(), [600, 1, 40_000, 5_000, 10, 8, 120, 1_100]);
|
||||
agent.SetRow(agent.Append(), [500, 1, 40_000, 5_000, 10, 7, 100, 1_000, 1]);
|
||||
agent.SetRow(agent.Append(), [600, 1, 40_000, 5_000, 10, 8, 120, 1_100, 1]);
|
||||
|
||||
var msg = EchoFragment((0, 1000, 3000));
|
||||
var fragment = new RawChunk(msg.Header, [msg.Streams[0], InfoRows(info, (100, 0, 1))]);
|
||||
@@ -227,6 +227,8 @@ public sealed class MeasurementTests
|
||||
Assert.Equal(3, merged.Count);
|
||||
Assert.Equal([0L, 1L, 1L], merged.Find("side")!.Values);
|
||||
Assert.Equal([1L, 7L, 8L], merged.Find("retrans_total")!.Values);
|
||||
Assert.False(merged.Find("conn")!.IsValid(0));
|
||||
Assert.Equal(1, merged.Find("conn")!.Values[1]);
|
||||
|
||||
// The agent's rows were consumed: the next chunk carries only the probe's own rows
|
||||
IReadOnlyList<RawStream> second = FragmentMerger.Merge(Kinds.TcpEcho, fragment, 1, recorder).Value;
|
||||
@@ -238,7 +240,7 @@ public sealed class MeasurementTests
|
||||
{
|
||||
StreamSpec info = Kinds.TcpEcho.FindStream("tcp_info")!;
|
||||
var agent = new RowStore(info, Side.Agent, 64);
|
||||
agent.SetRow(agent.Append(), [500, 1, 40_000, 5_000, 10, 7, 100, 1_000]);
|
||||
agent.SetRow(agent.Append(), [500, 1, 40_000, 5_000, 10, 7, 100, 1_000, 1]);
|
||||
|
||||
// A probe on a platform without kernel sampling sends the echo rows only
|
||||
RawChunk fragment = EchoFragment((0, 1000, 3000));
|
||||
@@ -248,6 +250,18 @@ public sealed class MeasurementTests
|
||||
Assert.Equal(7, rows.Find("retrans_total")!.Values[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FragmentMerger_LegacyTcpSamples_KeepConnectionIdentityUnknown()
|
||||
{
|
||||
StreamSpec info = Kinds.TcpEcho.FindStream("tcp_info")!;
|
||||
RawStream sample = InfoRows(info, (100, 0, 1));
|
||||
var legacy = new RawStream(sample.Name, sample.Count, sample.Columns.Where(c => c.Name != "conn").ToArray());
|
||||
var fragment = new RawChunk(new RawChunkHeader { Kind = "tcp_echo" }, [legacy]);
|
||||
RawStream merged = Assert.Single(FragmentMerger.Merge(Kinds.TcpEcho, fragment, 1, new Source()).Value);
|
||||
Assert.False(merged.Find("conn")!.IsValid(0));
|
||||
Assert.Equal(1, merged.Find("retrans_total")!.Values[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TcpInfo_OnLinux_ReadsTheKernelCountersOfALiveConnection()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Fleet.Core;
|
||||
|
||||
namespace MoShiro.Measurement.Tests;
|
||||
|
||||
public sealed class MsQuicConnectionTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(1200, true)]
|
||||
[InlineData(1280, true)]
|
||||
[InlineData(1472, false)]
|
||||
public void DatagramState_UsesTheNativeUint16Alignment(int maximum, bool enabled)
|
||||
{
|
||||
nint nativeEvent = Marshal.AllocHGlobal(IntPtr.Size + 4);
|
||||
try
|
||||
{
|
||||
Marshal.WriteByte(nativeEvent, IntPtr.Size, enabled ? (byte)1 : (byte)0);
|
||||
Marshal.WriteByte(nativeEvent, IntPtr.Size + 1, 0xA5); // padding is not part of MaxSendLength
|
||||
Marshal.WriteInt16(nativeEvent, IntPtr.Size + 2, (short)maximum);
|
||||
Assert.Equal((enabled, maximum), MsQuicConnection.ReadDatagramState(nativeEvent));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(nativeEvent);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DatagramConnection_CanSendCloseAndReconnect_WithoutBlockingNativeWorkers()
|
||||
{
|
||||
using ECDsa key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var request = new CertificateRequest("CN=localhost", key, HashAlgorithmName.SHA256);
|
||||
using X509Certificate2 certificate = request.CreateSelfSigned(DateTimeOffset.UnixEpoch, DateTimeOffset.UnixEpoch.AddYears(100));
|
||||
await using var listener = new MsQuicListener(IPAddress.Loopback, [0], certificate,
|
||||
(connection, data) => connection.Send(data), NullNodeLog.Instance, null);
|
||||
listener.Start();
|
||||
|
||||
for (int attempt = 0; attempt < 3; attempt++)
|
||||
{
|
||||
var reply = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
MsQuicConnection client = await MsQuicConnection.ConnectAsync(IPAddress.Loopback, listener.BoundPorts[0], 0,
|
||||
key.ExportSubjectPublicKeyInfo(), (_, data) => reply.TrySetResult(data.ToArray()), NullNodeLog.Instance, timeout.Token);
|
||||
try
|
||||
{
|
||||
while (!client.IsReady)
|
||||
{
|
||||
await Task.Delay(10, timeout.Token);
|
||||
}
|
||||
|
||||
Assert.InRange(client.MaxDatagramBytes, 64, 1500);
|
||||
byte[] payload = Enumerable.Range(0, 64).Select(i => (byte)i).ToArray();
|
||||
// DATAGRAM is unreliable, and the peer can receive the first
|
||||
// packet before its handshake-complete notification permits replies.
|
||||
while (!reply.Task.IsCompleted)
|
||||
{
|
||||
Assert.True(client.Send(payload));
|
||||
await Task.WhenAny(reply.Task, Task.Delay(20, timeout.Token));
|
||||
timeout.Token.ThrowIfCancellationRequested();
|
||||
}
|
||||
Assert.Equal(payload, await reply.Task.WaitAsync(timeout.Token));
|
||||
Assert.NotNull(client.TransportSnapshot);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// The old callback called RegistrationClose, deadlocking its own
|
||||
// worker. A second connection then hung in SetParam/GetParam.
|
||||
await client.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(3));
|
||||
await client.WaitForShutdownAsync().WaitAsync(TimeSpan.FromSeconds(3));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace MoShiro.Protocol.Tests;
|
||||
|
||||
public sealed class TcpWireTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(64)]
|
||||
[InlineData(16384)]
|
||||
public async Task Message_UsesOneWriteForPrefixAndBody_AndPreservesFraming(int size)
|
||||
{
|
||||
using var stream = new CountingStream();
|
||||
byte[] body = Enumerable.Range(0, size).Select(i => (byte)i).ToArray();
|
||||
await TcpWire.WriteMessageAsync(stream, body, default);
|
||||
Assert.Equal(1, stream.Writes);
|
||||
stream.Position = 0;
|
||||
Assert.Equal(body, await TcpWire.ReadMessageAsync(stream, default));
|
||||
Assert.Null(await TcpWire.ReadMessageAsync(stream, default));
|
||||
}
|
||||
|
||||
private sealed class CountingStream : MemoryStream
|
||||
{
|
||||
public int Writes { get; private set; }
|
||||
|
||||
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Writes++;
|
||||
return base.WriteAsync(buffer, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user