Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests/Cip/RequestSchedulerTests.cs
T

485 lines
22 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Fakes;
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Cip;
/// <summary>
/// Deterministic scheduler tests: requests are held in flight with <see cref="FakeCipChannel.Gate"/>
/// (or a per-call hold) and the queue is observed through test hooks, never with sleeps.
/// </summary>
public sealed class RequestSchedulerTests
{
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(10);
private static CancellationToken Ct => TestContext.Current.CancellationToken;
/// <summary>A request of exactly <paramref name="size"/> bytes, service 0x4C, tagged by <paramref name="tag"/>.</summary>
private static byte[] Req(int size, byte tag = 0)
{
var r = new byte[size];
r[0] = 0x4C;
if (size > 1) r[^1] = tag;
return r;
}
/// <summary>Puts <paramref name="first"/> in flight behind the channel gate and returns its task.</summary>
private static async Task<Task<CipReply>> HoldFirstAsync(RequestScheduler s, FakeCipChannel ch, byte[] first)
{
var expected = ch.SentCount + 1;
ch.Gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var t = s.SendAsync(first, CancellationToken.None);
await ch.WaitForSendsAsync(expected);
return t;
}
[Fact]
public async Task Requests_queued_behind_an_inflight_one_are_packed_into_one_msp()
{
var ch = new FakeCipChannel();
await using var s = new RequestScheduler(ch, allowPacking: true);
var a = await HoldFirstAsync(s, ch, Req(8, 1));
var b = s.SendAsync(Req(8, 2), CancellationToken.None);
var c = s.SendAsync(Req(8, 3), CancellationToken.None);
var d = s.SendAsync(Req(8, 4), CancellationToken.None);
s.QueuedCount.ShouldBe(3);
ch.Gate!.SetResult();
await Task.WhenAll(a, b, c, d).WaitAsync(Timeout, Ct);
ch.Sent.Count.ShouldBe(2);
ch.Sent[0].ShouldBe(Req(8, 1));
ch.Sent[1][0].ShouldBe((byte)0x0A);
FakeCipChannel.SplitMsp(ch.Sent[1]).ShouldBe([Req(8, 2), Req(8, 3), Req(8, 4)]); // FIFO
foreach (var t in new[] { a, b, c, d }) (await t).GeneralStatus.ShouldBe((byte)0);
}
[Fact]
public async Task Msp_exactly_at_limit_packs()
{
// MSP of two 10-byte requests = 6 + 2 + 2*2 + 20 = 32.
MultipleServicePacket.BuildSize([10, 10]).ShouldBe(32);
var ch = new FakeCipChannel(maxRequestBytes: 32);
await using var s = new RequestScheduler(ch, allowPacking: true);
var a = await HoldFirstAsync(s, ch, Req(4));
var x = s.SendAsync(Req(10, 1), CancellationToken.None);
var y = s.SendAsync(Req(10, 2), CancellationToken.None);
ch.Gate!.SetResult();
await Task.WhenAll(a, x, y).WaitAsync(Timeout, Ct);
ch.Sent.Count.ShouldBe(2);
ch.Sent[1].Length.ShouldBe(32);
FakeCipChannel.SplitMsp(ch.Sent[1]).ShouldBe([Req(10, 1), Req(10, 2)]);
}
[Fact]
public async Task Msp_one_byte_over_limit_splits()
{
var ch = new FakeCipChannel(maxRequestBytes: 31);
await using var s = new RequestScheduler(ch, allowPacking: true);
var a = await HoldFirstAsync(s, ch, Req(4));
var x = s.SendAsync(Req(10, 1), CancellationToken.None);
var y = s.SendAsync(Req(10, 2), CancellationToken.None);
ch.Gate!.SetResult();
await Task.WhenAll(a, x, y).WaitAsync(Timeout, Ct);
ch.Sent.Count.ShouldBe(3);
ch.Sent[1].ShouldBe(Req(10, 1));
ch.Sent[2].ShouldBe(Req(10, 2));
}
[Fact]
public async Task A_request_that_does_not_fit_never_lets_a_later_one_overtake_it()
{
var ch = new FakeCipChannel(maxRequestBytes: 100);
await using var s = new RequestScheduler(ch, allowPacking: true);
var a = await HoldFirstAsync(s, ch, Req(4, 1));
var b = s.SendAsync(Req(10, 2), CancellationToken.None);
var big = s.SendAsync(Req(400, 3), CancellationToken.None);
var d = s.SendAsync(Req(10, 4), CancellationToken.None);
ch.Gate!.SetResult();
await Task.WhenAll(a, b, big, d).WaitAsync(Timeout, Ct);
// b + big does not fit, so b goes bare; big exceeds the limit alone and goes bare; then d.
ch.Sent.Select(r => r[^1]).ShouldBe(new byte[] { 1, 2, 3, 4 });
ch.Sent.ShouldAllBe(r => r[0] == 0x4C);
}
[Fact]
public async Task A_single_request_larger_than_the_limit_is_sent_bare()
{
var ch = new FakeCipChannel(maxRequestBytes: 20);
await using var s = new RequestScheduler(ch, allowPacking: true);
var reply = await s.SendAsync(Req(30, 9), CancellationToken.None).WaitAsync(Timeout, Ct);
reply.GeneralStatus.ShouldBe((byte)0);
ch.Sent.Count.ShouldBe(1);
ch.Sent[0].ShouldBe(Req(30, 9));
}
[Fact]
public async Task Packing_disabled_sends_each_bare()
{
var ch = new FakeCipChannel();
await using var s = new RequestScheduler(ch, allowPacking: false);
var a = await HoldFirstAsync(s, ch, Req(8, 1));
var b = s.SendAsync(Req(8, 2), CancellationToken.None);
var c = s.SendAsync(Req(8, 3), CancellationToken.None);
ch.Gate!.SetResult();
await Task.WhenAll(a, b, c).WaitAsync(Timeout, Ct);
ch.Sent.Count.ShouldBe(3);
ch.Sent.Select(r => r[^1]).ShouldBe(new byte[] { 1, 2, 3 });
ch.Sent.ShouldAllBe(r => r[0] == 0x4C);
}
[Fact]
public async Task Per_service_error_fails_only_its_waiter()
{
var ch = new FakeCipChannel
{
RespondPerService = r => r[^1] == 2
? FakeCipChannel.Reply(r[0], 0x04, [], 0x0000)
: FakeCipChannel.Reply(r[0], 0x00, [0xAB]),
};
await using var s = new RequestScheduler(ch, allowPacking: true);
var a = await HoldFirstAsync(s, ch, Req(8, 0));
var ok = s.SendAsync(Req(8, 1), CancellationToken.None);
var bad = s.SendAsync(Req(8, 2), CancellationToken.None);
ch.Gate!.SetResult();
await Task.WhenAll(a, ok, bad).WaitAsync(Timeout, Ct);
ch.Sent.Count.ShouldBe(2);
var okReply = await ok;
okReply.GeneralStatus.ShouldBe((byte)0x00);
okReply.Status.ShouldBe(AbStatus.Ok);
okReply.Data.ToArray().ShouldBe(new byte[] { 0xAB });
var badReply = await bad;
badReply.GeneralStatus.ShouldBe((byte)0x04);
badReply.ExtendedStatus.ShouldBe(new ushort[] { 0x0000 });
}
[Fact]
public async Task Msp_envelope_failure_faults_every_waiter_in_the_batch_with_the_same_exception()
{
var ch = new FakeCipChannel();
await using var s = new RequestScheduler(ch, allowPacking: true);
var a = await HoldFirstAsync(s, ch, Req(8, 0));
ch.Respond = r => Task.FromResult(r[0] == 0x0A
? FakeCipChannel.Reply(0x0A, 0x08, []) // service not supported on the envelope
: FakeCipChannel.OkReply(r));
var x = s.SendAsync(Req(8, 1), CancellationToken.None);
var y = s.SendAsync(Req(8, 2), CancellationToken.None);
ch.Gate!.SetResult();
(await a.WaitAsync(Timeout, Ct)).GeneralStatus.ShouldBe((byte)0);
var ex1 = await Should.ThrowAsync<AbTransportException>(() => x.WaitAsync(Timeout, Ct));
var ex2 = await Should.ThrowAsync<AbTransportException>(() => y.WaitAsync(Timeout, Ct));
ex1.ShouldBeSameAs(ex2);
ex1.Status.ShouldBe(AbStatusCodes.FromCip(0x08, []));
}
[Fact]
public async Task Channel_failure_faults_every_waiter_in_the_batch()
{
var ch = new FakeCipChannel();
await using var s = new RequestScheduler(ch, allowPacking: true);
var a = await HoldFirstAsync(s, ch, Req(8, 0));
var boom = new AbTransportException(AbStatus.Timeout, "reply timeout");
ch.Respond = _ => Task.FromException<byte[]>(boom);
var x = s.SendAsync(Req(8, 1), CancellationToken.None);
var y = s.SendAsync(Req(8, 2), CancellationToken.None);
ch.Gate!.SetResult();
// a's own exchange also failed (the responder was swapped before release).
(await Should.ThrowAsync<AbTransportException>(() => a.WaitAsync(Timeout, Ct))).ShouldBeSameAs(boom);
(await Should.ThrowAsync<AbTransportException>(() => x.WaitAsync(Timeout, Ct))).ShouldBeSameAs(boom);
(await Should.ThrowAsync<AbTransportException>(() => y.WaitAsync(Timeout, Ct))).ShouldBeSameAs(boom);
// The scheduler survives a failed exchange.
ch.Respond = ch.DefaultRespond;
(await s.SendAsync(Req(8, 3), CancellationToken.None).WaitAsync(Timeout, Ct)).GeneralStatus.ShouldBe((byte)0);
}
[Fact]
public async Task A_non_transport_channel_exception_is_wrapped_and_shared_by_the_batch()
{
var ch = new FakeCipChannel();
await using var s = new RequestScheduler(ch, allowPacking: true);
var a = await HoldFirstAsync(s, ch, Req(8, 0));
ch.Respond = _ => Task.FromException<byte[]>(new IOException("socket reset"));
var x = s.SendAsync(Req(8, 1), CancellationToken.None);
var y = s.SendAsync(Req(8, 2), CancellationToken.None);
ch.Gate!.SetResult();
await Should.ThrowAsync<AbTransportException>(() => a.WaitAsync(Timeout, Ct));
var ex1 = await Should.ThrowAsync<AbTransportException>(() => x.WaitAsync(Timeout, Ct));
var ex2 = await Should.ThrowAsync<AbTransportException>(() => y.WaitAsync(Timeout, Ct));
ex1.ShouldBeSameAs(ex2);
ex1.InnerException.ShouldBeOfType<IOException>();
}
[Fact]
public async Task Queued_waiter_cancelled_before_send_is_never_sent()
{
var ch = new FakeCipChannel();
await using var s = new RequestScheduler(ch, allowPacking: true);
var a = await HoldFirstAsync(s, ch, Req(8, 1));
using var cts = new CancellationTokenSource();
var b = s.SendAsync(Req(8, 2), cts.Token);
s.QueuedCount.ShouldBe(1);
await cts.CancelAsync();
await Should.ThrowAsync<OperationCanceledException>(() => b.WaitAsync(Timeout, Ct));
b.IsCanceled.ShouldBeTrue();
s.QueuedCount.ShouldBe(0);
var c = s.SendAsync(Req(8, 3), CancellationToken.None);
ch.Gate!.SetResult();
await Task.WhenAll(a, c).WaitAsync(Timeout, Ct);
ch.Sent.Count.ShouldBe(2);
ch.Sent.Select(r => r[^1]).ShouldBe(new byte[] { 1, 3 });
ch.Sent[1][0].ShouldBe((byte)0x4C); // c went bare: the cancelled b was not packed with it
}
[Fact]
public async Task A_scheduler_fault_outside_the_exchange_fails_every_pending_waiter_and_keeps_pumping()
{
var ch = new FakeCipChannel();
await using var s = new RequestScheduler(ch, allowPacking: true);
var a = await HoldFirstAsync(s, ch, Req(8, 1));
var b = s.SendAsync(Req(8, 2), CancellationToken.None);
var c = s.SendAsync(Req(8, 3), CancellationToken.None);
var d = s.SendAsync(Req(8, 4), CancellationToken.None);
var broken = new InvalidOperationException("channel torn");
ch.MaxRequestBytesThrows = broken; // TakeBatch reads it when packing c behind b
ch.Gate!.SetResult();
(await a.WaitAsync(Timeout, Ct)).GeneralStatus.ShouldBe((byte)0);
var exB = await Should.ThrowAsync<AbTransportException>(() => b.WaitAsync(Timeout, Ct)); // already taken
var exC = await Should.ThrowAsync<AbTransportException>(() => c.WaitAsync(Timeout, Ct)); // still queued
var exD = await Should.ThrowAsync<AbTransportException>(() => d.WaitAsync(Timeout, Ct));
exB.Status.ShouldBe(AbStatus.BadConnection);
exB.InnerException.ShouldBeSameAs(broken);
exC.ShouldBeSameAs(exB);
exD.ShouldBeSameAs(exB);
s.QueuedCount.ShouldBe(0);
ch.SentCount.ShouldBe(1);
// The pump survived: a later request is answered, not left hanging.
ch.MaxRequestBytesThrows = null;
(await s.SendAsync(Req(8, 5), CancellationToken.None).WaitAsync(Timeout, Ct)).GeneralStatus.ShouldBe((byte)0);
}
[Fact]
public async Task A_token_already_cancelled_is_never_queued()
{
var ch = new FakeCipChannel();
await using var s = new RequestScheduler(ch, allowPacking: true);
var t = s.SendAsync(Req(8), new CancellationToken(canceled: true));
t.IsCanceled.ShouldBeTrue();
s.QueuedCount.ShouldBe(0);
s.LiveRegistrations.ShouldBe(0);
ch.SentCount.ShouldBe(0);
}
[Fact]
public async Task A_waiter_cancelled_while_its_batch_is_in_flight_is_abandoned_and_the_others_complete()
{
var ch = new FakeCipChannel();
var mspHold = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
ch.Respond = async r =>
{
if (r[0] == 0x0A) await mspHold.Task;
return await ch.DefaultRespond(r);
};
await using var s = new RequestScheduler(ch, allowPacking: true);
var a = await HoldFirstAsync(s, ch, Req(8, 1));
using var cts = new CancellationTokenSource();
var b = s.SendAsync(Req(8, 2), cts.Token);
var c = s.SendAsync(Req(8, 3), CancellationToken.None);
ch.Gate!.SetResult();
await ch.WaitForSendsAsync(2); // the MSP carrying b and c is now in flight
s.QueuedCount.ShouldBe(0);
await cts.CancelAsync();
await Should.ThrowAsync<OperationCanceledException>(() => b.WaitAsync(Timeout, Ct));
c.IsCompleted.ShouldBeFalse();
ch.Tokens[1].IsCancellationRequested.ShouldBeFalse(); // the shared exchange was not aborted
mspHold.SetResult();
(await c.WaitAsync(Timeout, Ct)).GeneralStatus.ShouldBe((byte)0);
(await a.WaitAsync(Timeout, Ct)).GeneralStatus.ShouldBe((byte)0);
s.LiveRegistrations.ShouldBe(0);
}
[Fact]
public async Task Waiter_cancellation_never_reaches_the_channel_token()
{
var ch = new FakeCipChannel();
await using var s = new RequestScheduler(ch, allowPacking: true);
using var w1 = new CancellationTokenSource();
using var w2 = new CancellationTokenSource();
using var w3 = new CancellationTokenSource();
var a = await HoldFirstAsync(s, ch, Req(8, 1)); // bare single, no waiter token
var single = s.SendAsync(Req(8, 2), w1.Token);
ch.Gate!.SetResult();
await Task.WhenAll(a, single).WaitAsync(Timeout, Ct); // bare single with a waiter token
var held = await HoldFirstAsync(s, ch, Req(8, 3));
var m1 = s.SendAsync(Req(8, 4), w2.Token);
var m2 = s.SendAsync(Req(8, 5), w3.Token);
ch.Gate!.SetResult();
await Task.WhenAll(held, m1, m2).WaitAsync(Timeout, Ct); // MSP with waiter tokens
ch.Sent.Count.ShouldBe(4);
ch.Sent[3][0].ShouldBe((byte)0x0A);
var waiterTokens = new[] { w1.Token, w2.Token, w3.Token };
ch.Tokens.ShouldAllBe(t => !waiterTokens.Contains(t));
ch.Tokens.Distinct().Count().ShouldBe(1); // every call used the scheduler's own stop token
ch.Tokens[0].CanBeCanceled.ShouldBeTrue();
}
[Fact]
public async Task Cancellation_registrations_are_released()
{
var ch = new FakeCipChannel();
await using var s = new RequestScheduler(ch, allowPacking: true);
using var longLived = new CancellationTokenSource();
// The hook really counts: queued waiters hold live registrations.
var a = await HoldFirstAsync(s, ch, Req(8));
var queued = Enumerable.Range(0, 5).Select(_ => s.SendAsync(Req(8), longLived.Token)).ToArray();
s.LiveRegistrations.ShouldBe(5);
ch.Gate!.SetResult();
await Task.WhenAll(queued.Append(a)).WaitAsync(Timeout, Ct);
s.LiveRegistrations.ShouldBe(0);
ch.Gate = null;
var tasks = new Task<CipReply>[10_000];
for (var i = 0; i < tasks.Length; i++) tasks[i] = s.SendAsync(Req(8), longLived.Token);
await Task.WhenAll(tasks).WaitAsync(TimeSpan.FromSeconds(60), Ct);
s.LiveRegistrations.ShouldBe(0);
// Cancelling the long-lived token afterwards touches nothing: every registration is gone.
await longLived.CancelAsync();
s.CancellationCallbacksRun.ShouldBe(0);
tasks.ShouldAllBe(t => t.IsCompletedSuccessfully);
}
[Fact]
public async Task Dispose_faults_queued_and_in_flight_waiters_as_a_communication_failure_not_a_cancellation()
{
var ch = new FakeCipChannel();
var s = new RequestScheduler(ch, allowPacking: true);
var a = await HoldFirstAsync(s, ch, Req(8, 1)); // in flight
using var own = new CancellationTokenSource();
var b = s.SendAsync(Req(8, 2), own.Token); // queued, with a live token
await s.DisposeAsync();
// Not OperationCanceledException: upstream would swallow that as "unsubscribed".
var inFlight = await Should.ThrowAsync<AbTransportException>(() => a.WaitAsync(Timeout, Ct));
inFlight.Status.ShouldBe(AbStatus.BadConnection);
var queued = await Should.ThrowAsync<AbTransportException>(() => b.WaitAsync(Timeout, Ct));
queued.Status.ShouldBe(AbStatus.BadConnection);
a.IsCanceled.ShouldBeFalse();
b.IsCanceled.ShouldBeFalse();
s.LiveRegistrations.ShouldBe(0);
ch.SentCount.ShouldBe(1);
Should.Throw<ObjectDisposedException>(() => s.SendAsync(Req(8), CancellationToken.None));
}
private static bool IsMsp(byte[] r) => r[0] == 0x0A;
[Fact]
public async Task An_exclusive_request_goes_out_bare_and_splits_the_packing_around_it_in_fifo_order()
{
var ch = new FakeCipChannel();
await using var s = new RequestScheduler(ch, allowPacking: true);
var a = await HoldFirstAsync(s, ch, Req(8, 1));
var b1 = s.SendAsync(Req(8, 2), Ct);
var b2 = s.SendAsync(Req(8, 3), Ct);
var c = s.SendAsync(Req(8, 4), Ct, exclusive: true);
var d1 = s.SendAsync(Req(8, 5), Ct);
var d2 = s.SendAsync(Req(8, 6), Ct);
s.QueuedCount.ShouldBe(5);
ch.Gate!.SetResult();
await Task.WhenAll(a, b1, b2, c, d1, d2).WaitAsync(Timeout, Ct);
// A, then [B1 B2] packed, C bare, then [D1 D2] packed: the exclusive request is a batch boundary.
ch.Sent.Count.ShouldBe(4);
IsMsp(ch.Sent[1]).ShouldBeTrue();
FakeCipChannel.SplitMsp(ch.Sent[1]).Select(e => e[^1]).ShouldBe(new byte[] { 2, 3 });
IsMsp(ch.Sent[2]).ShouldBeFalse();
ch.Sent[2][^1].ShouldBe((byte)4);
IsMsp(ch.Sent[3]).ShouldBeTrue();
FakeCipChannel.SplitMsp(ch.Sent[3]).Select(e => e[^1]).ShouldBe(new byte[] { 5, 6 });
}
[Fact]
public async Task An_exclusive_request_at_the_head_of_the_queue_does_not_take_followers_with_it()
{
var ch = new FakeCipChannel();
await using var s = new RequestScheduler(ch, allowPacking: true);
var a = await HoldFirstAsync(s, ch, Req(8, 1));
var c = s.SendAsync(Req(8, 2), Ct, exclusive: true);
var d1 = s.SendAsync(Req(8, 3), Ct);
var d2 = s.SendAsync(Req(8, 4), Ct);
ch.Gate!.SetResult();
await Task.WhenAll(a, c, d1, d2).WaitAsync(Timeout, Ct);
ch.Sent.Count.ShouldBe(3);
IsMsp(ch.Sent[1]).ShouldBeFalse();
ch.Sent[1][^1].ShouldBe((byte)2);
FakeCipChannel.SplitMsp(ch.Sent[2]).Select(e => e[^1]).ShouldBe(new byte[] { 3, 4 });
}
[Fact]
public async Task Mixed_scalar_string_and_array_replies_are_packed_within_the_reply_budget()
{
// Every request is 8 bytes, so the REQUEST budget would pack all of them into one MSP; only the
// expected-reply budget (#582) splits the batch. Reply envelope = 6 + 2n + sum(replies) <= 504.
const int scalar = 8, str = 96, array = 200;
var ch = new FakeCipChannel(maxRequestBytes: 504);
await using var s = new RequestScheduler(ch, allowPacking: true);
var a = await HoldFirstAsync(s, ch, Req(8, 1));
var sizes = new[] { scalar, str, str, str, str, array, scalar, RequestScheduler.UnknownReply };
var tasks = sizes.Select((size, i) => s.SendAsync(Req(8, (byte)(10 + i)), Ct, expectedReplyBytes: size)).ToArray();
ch.Gate!.SetResult();
await Task.WhenAll(tasks.Append(a)).WaitAsync(Timeout, Ct);
// [sc, S, S, S, S] = 6+10+392 = 408; the array (+200) would make 608 > 504, so it starts batch 2
// with the trailing scalar; the unknown-size read goes alone.
ch.Sent.Count.ShouldBe(4);
FakeCipChannel.SplitMsp(ch.Sent[1]).Select(e => e[^1]).ShouldBe(new byte[] { 10, 11, 12, 13, 14 });
FakeCipChannel.SplitMsp(ch.Sent[2]).Select(e => e[^1]).ShouldBe(new byte[] { 15, 16 });
IsMsp(ch.Sent[3]).ShouldBeFalse();
ch.Sent[3][^1].ShouldBe((byte)17);
ReplyEnvelopeFor(scalar, str, str, str, str).ShouldBeLessThanOrEqualTo(504);
ReplyEnvelopeFor(scalar, str, str, str, str, array).ShouldBeGreaterThan(504);
}
[Fact]
public async Task Status_only_replies_still_pack_to_the_request_limit()
{
// Positive control for the reply budget: the default (status-only) reply never splits a batch
// the request budget allows, so existing write / probe coalescing is unchanged.
var ch = new FakeCipChannel(maxRequestBytes: 504);
await using var s = new RequestScheduler(ch, allowPacking: true);
var a = await HoldFirstAsync(s, ch, Req(8, 1));
var rest = Enumerable.Range(0, 20).Select(i => s.SendAsync(Req(8, (byte)(2 + i)), Ct)).ToArray();
ch.Gate!.SetResult();
await Task.WhenAll(rest.Append(a)).WaitAsync(Timeout, Ct);
ch.Sent.Count.ShouldBe(2);
FakeCipChannel.SplitMsp(ch.Sent[1]).Count.ShouldBe(20);
}
private static int ReplyEnvelopeFor(params int[] replies) => RequestScheduler.ReplyEnvelopeSize(replies);
}