mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-21 17:16:16 +08:00
0edc3b55a6
Stacked on #4829 (publish timer sweep): this PR targets its branch, so the diff shows only this change. Retarget to master once #4829 merges. MonitoredItem.Publish rented every MonitoredItemNotification from the process-wide activator pool (BoundedObjectPool, bound 2 x monitored items, capped at 65536) and NotificationMessageLease returned it once the message was sent and acknowledged. Under load the pool costs more CPU than it saves: - A notification lives until its message is acknowledged, so a whole publishing cycle of notifications is in flight at once. With the publish sweep finishing within a few ms (fixed cadence + parallel remainder) the in-flight burst is far above the cap and most Gets miss anyway (MonitoredItemNotification 6 % -> 45 % of server allocation). - Raising the cap to 1M made it worse: 5.5 vs 4.4 us per notification, lock contention 16k vs 5k per window, delivery 93 vs 95.5 %. - Every hit touches an instance that went cold in the ~100-200 ms since it was returned, Reuse touches it once more, and the shared queue and counters bounce between cores. A fresh allocation comes from the thread's hot allocation context. The notifications are now allocated in MonitoredItem.Publish and left to the GC; RecyclePayload no longer reuses them, and the pool bound is only raised for EventFieldList. NotificationMessage, DataChangeNotification, EventNotificationList and EventFieldList pooling and the rented MonitoredItemNotification[] arrays are unchanged. Paired A/B on top of the publish sweep fix (perf/ServerLoadHarness, encrypted, alternating rounds, medians; runs repeated while processes outside the harness used more than 1.5 cores): | shape (sessions x subs x items) | GC | n | us/notif | server cores | delivered | GC pause / 15 s | p99 write->notify | |---------------------------------|--------|---|------------|--------------|--------------|-----------------|-------------------| | 50 x 15 x 600, pub 100, w 150 | server | 2 | 4.4 -> 3.1 | 11.9 -> 8.2 | 89 -> 89 % | 0.8 -> 1.5 s | 278 -> 325 ms | | 10 x 15 x 600, pub 100, w 150 | server | 3 | 5.1 -> 3.3 | 2.9 -> 2.0 | 100 -> 100 % | 15 -> 88 ms | 137 -> 141 ms | | 1000 x 1 x 1, pub 1000, w 500 | server | 7 | 158 -> 190 | 0.16 -> 0.19 | 50 -> 50 % | 15 -> 28 ms | 480 -> 490 ms | | 2 x 1 x 100, pub 100, w 150 | server | 3 | 59 -> 47 | 0.08 -> 0.06 | 100 -> 100 % | 7 -> 6 ms | 103 -> 114 ms | | 50 x 15 x 600, pub 100, w 150 | ws | 3*| 4.5 -> 3.5 | 10.5 -> 7.0 | 86 -> 96 % | 5.1 -> 8.6 s | 447 -> 373 ms | | 10 x 15 x 600, pub 100, w 150 | ws | 5*| 5.4 -> 3.6 | 3.3 -> 1.9 | 100 -> 100 % | 0.45 -> 1.1 s | 144 -> 155 ms | (*) workstation GC rows combine the A/B with the switch-build runs of the same comparison. Excluded: six runs (both builds) in which the 50-session shape collapsed with the server mostly idle (up to 16.7 s GC pause in the 15 s window, 0-4 % delivered), the GC suspension stall pattern seen before on this machine under external CPU saturation. The 1000-session cores differ by 0.03 with a same-build spread of 0.13-0.25 at ~1000 notifications/s. Trade-off: CPU per notification drops 25-40 % wherever notifications flow, but short-lived notifications now die young: gen0 GCs per 15 s 2 -> 24 (server GC, 10 sessions), gen1 35 -> 154 (workstation GC, 10 sessions), so GC pause time rises about 2x, and p99 latency rises with workstation GC at moderate load (+8 % at 10 sessions). When pooling could pay off again is tracked in #4819. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
504 lines
24 KiB
C#
504 lines
24 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Moq;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Tests;
|
|
|
|
namespace Opc.Ua.Server.Tests
|
|
{
|
|
/// <summary>
|
|
/// Verifies that a notification message keeps its pooled payload until the retransmission
|
|
/// queue and every Publish response that carries it have released it, and that copies
|
|
/// handed out for republish and mirroring are independent of queue recycling.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("Subscription")]
|
|
[NonParallelizable]
|
|
public sealed class NotificationEncodingLifetimeRegressionTests
|
|
{
|
|
/// <summary>
|
|
/// Verifies that delayed encoding preserves notifications after acknowledgement, eviction,
|
|
/// clearing, and pool reuse, and that a message shared with its response is recycled
|
|
/// only once the response was sent.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ReturnedNotificationSurvivesPoolReuseUntilEncodingCompletesAsync(
|
|
[Values("publish", "queued", "republish", "fullMirror", "deltaMirror")] string surface,
|
|
[Values("acknowledge", "evict", "clear")] string mutation,
|
|
[Values(false, true)] bool events)
|
|
{
|
|
NotificationMessage? mirrored = null;
|
|
var fullStore = new Mock<ISubscriptionRetransmissionStore>();
|
|
fullStore.Setup(store => store.StoreRetransmissionState(
|
|
1, It.IsAny<uint>(), It.IsAny<ArrayOf<NotificationMessage>>()))
|
|
.Callback<uint, uint, ArrayOf<NotificationMessage>>((_, _, values) => mirrored ??= values[0]);
|
|
var deltaStore = new Mock<ISubscriptionRetransmissionDeltaStore>();
|
|
deltaStore.Setup(store => store.StoreRetransmissionStateDelta(
|
|
1, It.IsAny<uint>(), It.IsAny<ArrayOf<NotificationMessage>>(), It.IsAny<ArrayOf<uint>>()))
|
|
.Callback<uint, uint, ArrayOf<NotificationMessage>, ArrayOf<uint>>(
|
|
(_, _, values, _) => mirrored ??= values[0]);
|
|
ISubscriptionRetransmissionStore? store = surface switch
|
|
{
|
|
"fullMirror" => fullStore.Object,
|
|
"deltaMirror" => deltaStore.Object,
|
|
_ => null
|
|
};
|
|
var queue = new SentMessageQueue(() => 1, 2, store, NullLogger.Instance);
|
|
var source = (NotificationMessage)NotificationMessageActivator.Instance.CreateInstance();
|
|
source.SequenceNumber = surface == "queued" ? 2u : 1u;
|
|
var item = (MonitoredItemNotification)MonitoredItemNotificationActivator.Instance.CreateInstance();
|
|
item.ClientHandle = 77;
|
|
item.Value = new DataValue(42);
|
|
var eventItem = (EventFieldList)EventFieldListActivator.Instance.CreateInstance();
|
|
eventItem.ClientHandle = 77;
|
|
eventItem.EventFields = [42];
|
|
var data = new DataChangeNotification { MonitoredItems = [item] };
|
|
var eventList = new EventNotificationList { Events = [eventItem] };
|
|
source.NotificationData = [new ExtensionObject(events ? eventList : data)];
|
|
List<NotificationMessage> messages = surface == "queued"
|
|
? [new NotificationMessage { SequenceNumber = 1 }, source]
|
|
: [source];
|
|
NotificationMessage published = queue.Enqueue(messages, [], out _, out _, out NotificationMessageLease publishedLease);
|
|
NotificationMessageLease? sharedLease = null;
|
|
NotificationMessage selected;
|
|
switch (surface)
|
|
{
|
|
case "queued":
|
|
publishedLease.Release();
|
|
selected = queue.TryDequeueQueued([], false, out _, out sharedLease)!;
|
|
break;
|
|
case "publish":
|
|
sharedLease = publishedLease;
|
|
selected = published;
|
|
break;
|
|
default:
|
|
// the response that carried the message was sent long ago.
|
|
publishedLease.Release();
|
|
selected = surface == "republish"
|
|
? queue.FindForRepublish(source.SequenceNumber)!
|
|
: mirrored!;
|
|
break;
|
|
}
|
|
bool shared = sharedLease != null;
|
|
Assert.That(ReferenceEquals(selected, source), Is.EqualTo(shared),
|
|
"Publish shares the retained message; republish and mirrors copy it.");
|
|
|
|
uint expectedSequence = source.SequenceNumber;
|
|
var releaseEncoder = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
PublishResponse response = shared
|
|
? new LeasedPublishResponse(sharedLease) { NotificationMessage = selected }
|
|
: new PublishResponse { NotificationMessage = selected };
|
|
Task<PublishResponse> encoding = EncodeAfterReleaseAsync(response, releaseEncoder.Task);
|
|
try
|
|
{
|
|
if (mutation == "acknowledge")
|
|
{
|
|
Assert.That(queue.TryAcknowledge(expectedSequence), Is.True);
|
|
}
|
|
else if (mutation == "evict")
|
|
{
|
|
queue.Enqueue(
|
|
[new NotificationMessage { SequenceNumber = 3 }, new NotificationMessage { SequenceNumber = 4 }],
|
|
[], out _, out _);
|
|
}
|
|
else
|
|
{
|
|
queue.Clear();
|
|
}
|
|
|
|
// a payload still owned by a response being sent is not recycled.
|
|
Assert.That(source.IsEmpty, Is.EqualTo(!shared));
|
|
}
|
|
finally
|
|
{
|
|
releaseEncoder.TrySetResult(true);
|
|
}
|
|
var reused = (NotificationMessage)NotificationMessageActivator.Instance.CreateInstance();
|
|
reused.SequenceNumber = 999;
|
|
PublishResponse decoded;
|
|
try
|
|
{
|
|
decoded = await encoding.ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
reused.Reuse();
|
|
}
|
|
|
|
// the transport has sent the response: the last owner recycles the payload.
|
|
((IPooledEncodeable)response).Reuse();
|
|
Assert.That(source.IsEmpty, Is.True);
|
|
|
|
Assert.That(decoded.NotificationMessage.SequenceNumber, Is.EqualTo(expectedSequence));
|
|
Assert.That(decoded.NotificationMessage.NotificationData, Has.Count.EqualTo(1));
|
|
if (events)
|
|
{
|
|
Assert.That(decoded.NotificationMessage.NotificationData[0].TryGetValue(out EventNotificationList? value),
|
|
Is.True);
|
|
Assert.That(value!.Events[0].ClientHandle, Is.EqualTo(77u));
|
|
Assert.That(value.Events[0].EventFields[0].GetInt32(), Is.EqualTo(42));
|
|
Assert.That(eventItem.ClientHandle, Is.Zero);
|
|
item.Reuse();
|
|
}
|
|
else
|
|
{
|
|
Assert.That(decoded.NotificationMessage.NotificationData[0].TryGetValue(out DataChangeNotification? value),
|
|
Is.True);
|
|
Assert.That(value!.MonitoredItems[0].ClientHandle, Is.EqualTo(77u));
|
|
Assert.That(value.MonitoredItems[0].Value.WrappedValue.GetInt32(), Is.EqualTo(42));
|
|
|
|
// data change notifications are not pooled by the server; they are left to the GC.
|
|
Assert.That(item.ClientHandle, Is.EqualTo(77u));
|
|
item.Reuse();
|
|
eventItem.Reuse();
|
|
}
|
|
queue.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// A client may acknowledge a sequence number before the server finished sending the
|
|
/// response that carries it. The acknowledgement must not recycle the payload that is
|
|
/// still being encoded; the send completion does.
|
|
/// </summary>
|
|
[Test]
|
|
public void AcknowledgeBeforeSendCompletionDoesNotRecyclePayloadStillBeingSent()
|
|
{
|
|
var queue = new SentMessageQueue(() => 1, 4, null, NullLogger.Instance);
|
|
var message = new CountingNotificationMessage { SequenceNumber = 1 };
|
|
var item = new CountingMonitoredItemNotification { ClientHandle = 5, Value = new DataValue(7) };
|
|
message.NotificationData = [new ExtensionObject(new DataChangeNotification { MonitoredItems = [item] })];
|
|
|
|
NotificationMessage sent = queue.Enqueue([message], [], out _, out _, out NotificationMessageLease lease);
|
|
var response = new LeasedPublishResponse(lease) { NotificationMessage = sent };
|
|
|
|
Assert.That(queue.TryAcknowledge(1), Is.True);
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(message.ReuseCount, Is.Zero, "acknowledged before it was sent");
|
|
Assert.That(item.ReuseCount, Is.Zero);
|
|
Assert.That(Encode(response).NotificationMessage.SequenceNumber, Is.EqualTo(1u));
|
|
});
|
|
|
|
((IPooledEncodeable)response).Reuse();
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(message.ReuseCount, Is.EqualTo(1));
|
|
|
|
// data change notifications are not pooled by the server and never reused.
|
|
Assert.That(item.ReuseCount, Is.Zero);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// The send completion before the acknowledgement keeps the retained message for
|
|
/// Republish, which returns the original content.
|
|
/// </summary>
|
|
[Test]
|
|
public void RepublishAfterSendCompletedReturnsOriginalContent()
|
|
{
|
|
var queue = new SentMessageQueue(() => 1, 4, null, NullLogger.Instance);
|
|
var message = new CountingNotificationMessage { SequenceNumber = 1 };
|
|
var item = new CountingMonitoredItemNotification { ClientHandle = 5, Value = new DataValue(7) };
|
|
message.NotificationData = [new ExtensionObject(new DataChangeNotification { MonitoredItems = [item] })];
|
|
|
|
NotificationMessage sent = queue.Enqueue([message], [], out _, out _, out NotificationMessageLease lease);
|
|
var response = new LeasedPublishResponse(lease) { NotificationMessage = sent };
|
|
Encode(response);
|
|
((IPooledEncodeable)response).Reuse();
|
|
Assert.That(message.ReuseCount, Is.Zero, "still retained for republish");
|
|
|
|
NotificationMessage republished = queue.FindForRepublish(1)!;
|
|
Assert.That(republished, Is.Not.SameAs(message));
|
|
Assert.That(republished.IsEqual(message), Is.True);
|
|
|
|
Assert.That(queue.TryAcknowledge(1), Is.True);
|
|
Assert.That(message.ReuseCount, Is.EqualTo(1));
|
|
|
|
// the republished copy is independent of the recycled original.
|
|
PublishResponse decoded = Encode(new PublishResponse { NotificationMessage = republished });
|
|
Assert.That(decoded.NotificationMessage.SequenceNumber, Is.EqualTo(1u));
|
|
Assert.That(
|
|
decoded.NotificationMessage.NotificationData[0].TryGetValue(out DataChangeNotification? data),
|
|
Is.True);
|
|
Assert.That(data!.MonitoredItems[0].ClientHandle, Is.EqualTo(5u));
|
|
Assert.That(data.MonitoredItems[0].Value.WrappedValue.GetInt32(), Is.EqualTo(7));
|
|
}
|
|
|
|
/// <summary>
|
|
/// When sending fails the transport drops the response, and a failure path may release
|
|
/// it more than once (fault after a failed send, retried send on another channel). The
|
|
/// payload is released exactly once and only after the queue released it too.
|
|
/// </summary>
|
|
[Test]
|
|
public void ChannelFailureMidSendReleasesPayloadExactlyOnce(
|
|
[Values(false, true)] bool acknowledgeFirst)
|
|
{
|
|
var queue = new SentMessageQueue(() => 1, 4, null, NullLogger.Instance);
|
|
var message = new CountingNotificationMessage { SequenceNumber = 1 };
|
|
message.NotificationData = [new ExtensionObject(new DataChangeNotification
|
|
{
|
|
MonitoredItems = [new CountingMonitoredItemNotification { ClientHandle = 5 }]
|
|
})];
|
|
NotificationMessage sent = queue.Enqueue([message], [], out _, out _, out NotificationMessageLease lease);
|
|
var response = new LeasedPublishResponse(lease) { NotificationMessage = sent };
|
|
|
|
// a copy made while the response was in flight owns no reference.
|
|
var copy = (PublishResponse)response.Clone();
|
|
((IPooledEncodeable)copy).Reuse();
|
|
|
|
if (acknowledgeFirst)
|
|
{
|
|
Assert.That(queue.TryAcknowledge(1), Is.True);
|
|
}
|
|
|
|
// the send failed: the listener releases the response, then its error path again.
|
|
((IPooledEncodeable)response).Reuse();
|
|
((IPooledEncodeable)response).Reuse();
|
|
Assert.That(message.ReuseCount, Is.EqualTo(acknowledgeFirst ? 1 : 0));
|
|
|
|
if (!acknowledgeFirst)
|
|
{
|
|
// the client never got it and republishes; it acknowledges afterwards.
|
|
Assert.That(queue.FindForRepublish(1), Is.Not.Null);
|
|
Assert.That(queue.TryAcknowledge(1), Is.True);
|
|
}
|
|
|
|
queue.Clear();
|
|
Assert.That(message.ReuseCount, Is.EqualTo(1));
|
|
Assert.That(lease.References, Is.Zero);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The rented array that holds the data change notifications of a message is returned
|
|
/// to its pool with the payload, after the acknowledgement and the send completion,
|
|
/// and the message encodes only its slice of it.
|
|
/// </summary>
|
|
[Test]
|
|
public void RentedNotificationArrayIsReturnedWithThePayload([Values(false, true)] bool acknowledgeFirst)
|
|
{
|
|
var queue = new SentMessageQueue(() => 1, 4, null, NullLogger.Instance);
|
|
MonitoredItemNotification[] rented = System.Buffers.ArrayPool<MonitoredItemNotification>.Shared.Rent(3);
|
|
for (int ii = 0; ii < 3; ii++)
|
|
{
|
|
rented[ii] = new MonitoredItemNotification { ClientHandle = (uint)ii + 1, Value = new DataValue(ii) };
|
|
}
|
|
var message = new NotificationMessage
|
|
{
|
|
SequenceNumber = 1,
|
|
NotificationData = [new ExtensionObject(new DataChangeNotification
|
|
{
|
|
MonitoredItems = new ArrayOf<MonitoredItemNotification>(rented.AsMemory(0, 3))
|
|
})]
|
|
};
|
|
|
|
MonitoredItemNotification[] originals = rented.Take(3).ToArray();
|
|
NotificationMessage sent = queue.Enqueue(
|
|
[message], [], out _, out _, out NotificationMessageLease lease, [rented]);
|
|
var response = new LeasedPublishResponse(lease) { NotificationMessage = sent };
|
|
PublishResponse decoded = Encode(response);
|
|
Assert.That(
|
|
decoded.NotificationMessage.NotificationData[0].TryGetValue(out DataChangeNotification? data),
|
|
Is.True);
|
|
Assert.That(data!.MonitoredItems.Count, Is.EqualTo(3));
|
|
Assert.That(data.MonitoredItems[2].ClientHandle, Is.EqualTo(3u));
|
|
|
|
if (acknowledgeFirst)
|
|
{
|
|
Assert.That(queue.TryAcknowledge(1), Is.True);
|
|
Assert.That(rented[0], Is.SameAs(originals[0]), "still being sent");
|
|
((IPooledEncodeable)response).Reuse();
|
|
}
|
|
else
|
|
{
|
|
((IPooledEncodeable)response).Reuse();
|
|
Assert.That(rented[0], Is.SameAs(originals[0]), "still retained for republish");
|
|
Assert.That(queue.TryAcknowledge(1), Is.True);
|
|
}
|
|
|
|
// returned with clearArray, so it no longer references the recycled notifications
|
|
// (another test may have rented it again since).
|
|
for (int ii = 0; ii < originals.Length; ii++)
|
|
{
|
|
Assert.That(rented[ii], Is.Not.SameAs(originals[ii]));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Messages dropped unsent on queue overflow return their rented arrays.
|
|
/// </summary>
|
|
[Test]
|
|
public void OverflowReturnsTheRentedArraysOfDroppedMessages()
|
|
{
|
|
var queue = new SentMessageQueue(() => 1, 1, null, NullLogger.Instance);
|
|
var droppedItem = new MonitoredItemNotification { ClientHandle = 1 };
|
|
var keptItem = new MonitoredItemNotification { ClientHandle = 2 };
|
|
MonitoredItemNotification[] dropped = System.Buffers.ArrayPool<MonitoredItemNotification>.Shared.Rent(1);
|
|
dropped[0] = droppedItem;
|
|
MonitoredItemNotification[] kept = System.Buffers.ArrayPool<MonitoredItemNotification>.Shared.Rent(1);
|
|
kept[0] = keptItem;
|
|
List<NotificationMessage> messages =
|
|
[
|
|
new NotificationMessage { SequenceNumber = 1 },
|
|
new NotificationMessage { SequenceNumber = 2 }
|
|
];
|
|
List<MonitoredItemNotification[]?> rented = [dropped, kept];
|
|
|
|
NotificationMessage sent = queue.Enqueue(messages, [], out _, out uint discarded, out NotificationMessageLease lease, rented);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(sent.SequenceNumber, Is.EqualTo(2u));
|
|
Assert.That(discarded, Is.EqualTo(1u));
|
|
Assert.That(dropped[0], Is.Not.SameAs(droppedItem), "returned with the dropped message");
|
|
Assert.That(kept[0], Is.SameAs(keptItem));
|
|
Assert.That(rented, Has.Count.EqualTo(1), "trimmed with the messages");
|
|
});
|
|
lease.Release();
|
|
queue.Clear();
|
|
Assert.That(kept[0], Is.Not.SameAs(keptItem));
|
|
}
|
|
|
|
/// <summary>
|
|
/// A release beyond the owners never recycles the payload a second time, and a
|
|
/// later regular release still finds the count intact.
|
|
/// </summary>
|
|
[Test]
|
|
public void ExtraReleaseNeverRecyclesTwice()
|
|
{
|
|
var message = new CountingNotificationMessage { SequenceNumber = 1 };
|
|
var lease = new NotificationMessageLease(message);
|
|
lease.Release();
|
|
lease.Release();
|
|
Assert.That(message.ReuseCount, Is.EqualTo(1));
|
|
Assert.That(lease.References, Is.Zero);
|
|
|
|
var shared = new CountingNotificationMessage { SequenceNumber = 2 };
|
|
var sharedLease = new NotificationMessageLease(shared);
|
|
sharedLease.AddReference();
|
|
sharedLease.Release();
|
|
Assert.That(shared.ReuseCount, Is.Zero, "one owner left");
|
|
sharedLease.Release();
|
|
sharedLease.Release();
|
|
Assert.That(shared.ReuseCount, Is.EqualTo(1));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The retained messages are exposed read-only, in queue order, as the shared
|
|
/// instances (not copies).
|
|
/// </summary>
|
|
[Test]
|
|
public void SentMessagesExposesTheRetainedMessagesInOrder()
|
|
{
|
|
var queue = new SentMessageQueue(() => 1, 4, null, NullLogger.Instance);
|
|
var first = new NotificationMessage { SequenceNumber = 1 };
|
|
var second = new NotificationMessage { SequenceNumber = 2 };
|
|
queue.Enqueue([first, second], [], out _, out _, out NotificationMessageLease lease);
|
|
lease.Release();
|
|
|
|
IReadOnlyList<NotificationMessage> sent = queue.SentMessages;
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(sent, Has.Count.EqualTo(2));
|
|
Assert.That(sent[1], Is.SameAs(second));
|
|
Assert.That(sent.ToList(), Is.EqualTo(new[] { first, second }));
|
|
Assert.That(((System.Collections.IEnumerable)sent).Cast<NotificationMessage>().First(), Is.SameAs(first));
|
|
});
|
|
queue.Clear();
|
|
Assert.That(sent, Is.Empty);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A keep-alive is not retained for republish, so its response is the only owner and
|
|
/// recycles it once it was sent.
|
|
/// </summary>
|
|
[Test]
|
|
public void KeepAliveMessageIsOwnedByItsResponseOnly()
|
|
{
|
|
var keepAlive = new CountingNotificationMessage { SequenceNumber = 3 };
|
|
var lease = new NotificationMessageLease(keepAlive);
|
|
var response = new LeasedPublishResponse(lease) { NotificationMessage = keepAlive };
|
|
Assert.That(keepAlive.ReuseCount, Is.Zero);
|
|
((IPooledEncodeable)response).Reuse();
|
|
Assert.That(keepAlive.ReuseCount, Is.EqualTo(1));
|
|
}
|
|
|
|
private static PublishResponse Encode(PublishResponse response)
|
|
{
|
|
ServiceMessageContext context = ServiceMessageContext.Create(NUnitTelemetryContext.Create());
|
|
using var encoder = new BinaryEncoder(context);
|
|
response.Encode(encoder);
|
|
using var decoder = new BinaryDecoder(encoder.CloseAndReturnBuffer()!, context);
|
|
var decoded = new PublishResponse();
|
|
decoded.Decode(decoder);
|
|
return decoded;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Waits for queue mutation before round-tripping the retained publish response through binary encoding.
|
|
/// </summary>
|
|
private static async Task<PublishResponse> EncodeAfterReleaseAsync(PublishResponse response, Task release)
|
|
{
|
|
await release.ConfigureAwait(false);
|
|
return Encode(response);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Counts recycling instead of returning the instance to the shared pool.
|
|
/// </summary>
|
|
private sealed class CountingNotificationMessage : NotificationMessage
|
|
{
|
|
public int ReuseCount { get; private set; }
|
|
|
|
protected override void ReuseCore()
|
|
{
|
|
ReuseCount++;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Counts recycling instead of returning the instance to the shared pool.
|
|
/// </summary>
|
|
private sealed class CountingMonitoredItemNotification : MonitoredItemNotification
|
|
{
|
|
public int ReuseCount { get; private set; }
|
|
|
|
protected override void ReuseCore()
|
|
{
|
|
ReuseCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|