test(abcip): failing repro for STAB-4 unserialized shared-handle ops
This commit is contained in:
@@ -28,7 +28,7 @@
|
|||||||
{
|
{
|
||||||
"id": "B2.1",
|
"id": "B2.1",
|
||||||
"subject": "B2: failing AbCipRuntimeConcurrencyTests (reentrancy-guarded FakeAbCipTag hammer) \u2014 STAB-4 repro",
|
"subject": "B2: failing AbCipRuntimeConcurrencyTests (reentrancy-guarded FakeAbCipTag hammer) \u2014 STAB-4 repro",
|
||||||
"status": "pending",
|
"status": "completed",
|
||||||
"blockedBy": []
|
"blockedBy": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Regression coverage for 05/STAB-4 — a per-tag libplctag runtime (a single <c>Tag</c>
|
||||||
|
/// handle) is cached in <c>DeviceState.Runtimes</c> and shared between the server read path,
|
||||||
|
/// every poll-group loop, and the alarm-projection loop. A <c>Tag</c> handle is not safe for
|
||||||
|
/// concurrent Read/GetStatus/Decode (or Encode/Write/GetStatus), so the driver must serialise
|
||||||
|
/// the whole sequence per runtime — mirrors <c>AbLegacyRuntimeConcurrencyTests</c>.
|
||||||
|
/// </summary>
|
||||||
|
[Trait("Category", "Unit")]
|
||||||
|
public sealed class AbCipRuntimeConcurrencyTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A fake runtime that records the maximum number of operations in flight against the
|
||||||
|
/// <em>same</em> handle. If the driver fails to serialise, two callers overlap inside the
|
||||||
|
/// Read → GetStatus → Decode window and <see cref="MaxConcurrent"/> exceeds 1.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class OverlapDetectingFake : FakeAbCipTag
|
||||||
|
{
|
||||||
|
private int _inFlight;
|
||||||
|
|
||||||
|
/// <summary>Gets the maximum number of concurrent operations detected on this handle.</summary>
|
||||||
|
public int MaxConcurrent { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance of the <see cref="OverlapDetectingFake"/> class.</summary>
|
||||||
|
/// <param name="p">The tag creation parameters.</param>
|
||||||
|
public OverlapDetectingFake(AbCipTagCreateParams p) : base(p) { }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task ReadAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
EnterOp();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Yield + small delay so an unserialised second caller is guaranteed to overlap.
|
||||||
|
await Task.Delay(15, cancellationToken).ConfigureAwait(false);
|
||||||
|
await base.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally { LeaveOp(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task WriteAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
EnterOp();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(15, cancellationToken).ConfigureAwait(false);
|
||||||
|
await base.WriteAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally { LeaveOp(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnterOp()
|
||||||
|
{
|
||||||
|
var n = Interlocked.Increment(ref _inFlight);
|
||||||
|
lock (this) { if (n > MaxConcurrent) MaxConcurrent = n; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LeaveOp() => Interlocked.Decrement(ref _inFlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AbCipDriver NewDriver(FakeAbCipTagFactory factory, params AbCipTagDefinition[] tags)
|
||||||
|
{
|
||||||
|
var opts = new AbCipDriverOptions
|
||||||
|
{
|
||||||
|
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
|
||||||
|
Tags = tags,
|
||||||
|
};
|
||||||
|
return new AbCipDriver(opts, "drv-1", factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that concurrent reads of the same tag are serialised against the shared runtime.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Concurrent_reads_of_same_tag_are_serialised_against_the_shared_runtime()
|
||||||
|
{
|
||||||
|
OverlapDetectingFake? shared = null;
|
||||||
|
var factory = new FakeAbCipTagFactory
|
||||||
|
{
|
||||||
|
Customise = p => shared = new OverlapDetectingFake(p) { Value = 7 },
|
||||||
|
};
|
||||||
|
var drv = NewDriver(factory,
|
||||||
|
new AbCipTagDefinition("X", "ab://10.0.0.5/1,0", "Motor1.Speed", AbCipDataType.DInt));
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
|
||||||
|
// Eight callers race for the same tag — mimics the server read path + poll loop(s) + the
|
||||||
|
// alarm-projection loop hitting one cached runtime at once.
|
||||||
|
var reads = Enumerable.Range(0, 8)
|
||||||
|
.Select(_ => drv.ReadAsync(["X"], CancellationToken.None))
|
||||||
|
.ToArray();
|
||||||
|
await Task.WhenAll(reads);
|
||||||
|
|
||||||
|
shared.ShouldNotBeNull();
|
||||||
|
shared!.MaxConcurrent.ShouldBe(1, "operations on a shared libplctag Tag must not overlap");
|
||||||
|
reads.ShouldAllBe(r => r.Result.Single().Value!.Equals(7));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that a concurrent read and write of the same tag do not overlap on the shared runtime.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Concurrent_read_and_write_of_same_tag_do_not_overlap()
|
||||||
|
{
|
||||||
|
OverlapDetectingFake? shared = null;
|
||||||
|
var factory = new FakeAbCipTagFactory
|
||||||
|
{
|
||||||
|
Customise = p => shared = new OverlapDetectingFake(p) { Value = 1 },
|
||||||
|
};
|
||||||
|
var drv = NewDriver(factory,
|
||||||
|
new AbCipTagDefinition("X", "ab://10.0.0.5/1,0", "Motor1.Speed", AbCipDataType.DInt));
|
||||||
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||||
|
|
||||||
|
var readTask = drv.ReadAsync(["X"], CancellationToken.None);
|
||||||
|
var writeTask = drv.WriteAsync([new WriteRequest("X", 99)], CancellationToken.None);
|
||||||
|
await Task.WhenAll(readTask, writeTask);
|
||||||
|
|
||||||
|
shared.ShouldNotBeNull();
|
||||||
|
shared!.MaxConcurrent.ShouldBe(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user