mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-27 13:10:37 +08:00
Server: advance a subscription's publish deadline by one interval per expiry
With the fixed-cadence sweep running at the publishing interval, each subscription's deadline keeps one phase relative to the sweep for the whole session. When that phase falls inside the ~16 ms Windows timer jitter, a late sweep followed by an on-time one finds the subscription a whole interval behind, and the advance loop jumped past two deadlines: a publishing cycle was dropped, and with queue size 1 the item values of that cycle were overwritten. Runs became bimodal (100 % or 56-85 % delivered) depending on the phase drawn at setup. The deadline now moves by one interval per expiry, so the next sweep processes the expiry that was due; only a subscription still behind by a whole interval after that (a stalled sweep) skips the missed expiries as before. 2 sessions x 1 subscription x 100 items, write = publish = 100 ms, security none, 10 s window, paired and idle-gated: - before: 6 of 24 runs at 56-85 %, the rest 96-101 % - after: 24 of 24 runs at 97-101 % - master: steady 88-98 % With write 150 ms: 98.5-100 % (before: one run at 89.6 %). The 50 x 15 x 600 LoadTest shape is unchanged (98.8-100.7 %). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f0b1d0348b
commit
f33893b7c4
@@ -781,10 +781,20 @@ namespace Opc.Ua.Server
|
||||
return PublishingState.Idle;
|
||||
}
|
||||
|
||||
// set next expiry time, strictly after the current time.
|
||||
while (m_publishTimerExpiry <= currentTime)
|
||||
// set next expiry time. The publish timer sweep runs on its own cadence, and
|
||||
// when it runs at the publishing interval a sweep that wakes a little late
|
||||
// can find the subscription a whole interval behind. Advancing by one
|
||||
// interval keeps that expiry due for the next sweep instead of dropping a
|
||||
// publishing cycle; only a subscription that is still behind by a whole
|
||||
// interval after that (a stalled sweep) skips the missed expiries.
|
||||
long publishingInterval = (long)m_publishingInterval;
|
||||
m_publishTimerExpiry += publishingInterval;
|
||||
if (m_publishTimerExpiry <= currentTime - publishingInterval)
|
||||
{
|
||||
m_publishTimerExpiry += (long)m_publishingInterval;
|
||||
while (m_publishTimerExpiry <= currentTime)
|
||||
{
|
||||
m_publishTimerExpiry += publishingInterval;
|
||||
}
|
||||
}
|
||||
|
||||
// OPC 10000-4 §5.14.1.2 row 27: the LifetimeCounter is decremented when the
|
||||
|
||||
@@ -156,6 +156,63 @@ namespace Opc.Ua.Server.Tests
|
||||
Assert.That(subscription.Diagnostics.CurrentLifetimeCount, Is.EqualTo((uint)steps.Length));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A sweep that runs at the publishing interval and wakes a little early or late
|
||||
/// around the deadline does not drop publishing cycles. The deadline moves by one
|
||||
/// interval per expiry, so a sweep that finds the subscription a whole interval
|
||||
/// behind because of timer jitter keeps the next expiry due for the next sweep.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void SweepJitterAroundTheDeadlineDoesNotDropCycles()
|
||||
{
|
||||
var clock = new FakeTimeProvider();
|
||||
using Subscription subscription = CreateSubscription(
|
||||
clock,
|
||||
maxLifetimeCount: 1000,
|
||||
maxKeepAliveCount: 300);
|
||||
|
||||
// deadlines at 100, 200, 300, ...; sweeps at 95, 205, 295, 405, 495, 605, 695, 805.
|
||||
long[] steps = [95, 110, 90, 110, 90, 110, 90, 110];
|
||||
foreach (long step in steps)
|
||||
{
|
||||
clock.Advance(TimeSpan.FromMilliseconds(step));
|
||||
subscription.PublishTimerExpired();
|
||||
}
|
||||
|
||||
// the sweep at 95 is early; every later sweep processes one expiry.
|
||||
Assert.That(subscription.Diagnostics.CurrentLifetimeCount, Is.EqualTo(7u));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A subscription that fell behind by more than a publishing interval (a stalled
|
||||
/// sweep) skips the missed expiries instead of catching up with a burst.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void StalledSweepSkipsMissedExpiries()
|
||||
{
|
||||
var clock = new FakeTimeProvider();
|
||||
using Subscription subscription = CreateSubscription(
|
||||
clock,
|
||||
maxLifetimeCount: 1000,
|
||||
maxKeepAliveCount: 300);
|
||||
|
||||
clock.Advance(TimeSpan.FromMilliseconds(101));
|
||||
subscription.PublishTimerExpired();
|
||||
|
||||
// the sweep stalls for ten intervals; the late sweep processes one expiry.
|
||||
clock.Advance(TimeSpan.FromMilliseconds(1000));
|
||||
subscription.PublishTimerExpired();
|
||||
Assert.That(subscription.Diagnostics.CurrentLifetimeCount, Is.EqualTo(2u));
|
||||
|
||||
// the next expiry is the first deadline after the late sweep (1200 ms).
|
||||
clock.Advance(TimeSpan.FromMilliseconds(50));
|
||||
subscription.PublishTimerExpired();
|
||||
Assert.That(subscription.Diagnostics.CurrentLifetimeCount, Is.EqualTo(2u));
|
||||
clock.Advance(TimeSpan.FromMilliseconds(50));
|
||||
subscription.PublishTimerExpired();
|
||||
Assert.That(subscription.Diagnostics.CurrentLifetimeCount, Is.EqualTo(3u));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M1-1: a client that keeps answering keep-alives never lets the lifetime expire.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user