fix(mtconnect): the Agent refuses count ABOVE bufferSize, not at it (#825)
Live gate against the cppagent 2.7 fixture: count=16384 with bufferSize
16384 streams; 16385 is OUT_OF_RANGE ('count' must be less than 16385).
Warn only when SampleCount > bufferSize, pin both sides of the edge
against the live Agent, and correct the #804 wording that said 'at or
above'.
This commit is contained in:
@@ -296,7 +296,7 @@ host logger factory (wired in `DriverFactoryBootstrap`; the FOCAS and TwinCAT fa
|
||||
`InvalidDataException` — before #804 the 400 was a transport fault and the `OUT_OF_RANGE` re-baseline never ran
|
||||
live. The restart (`instanceId`) test also runs on every re-baseline snapshot (on 2.x that is where a restart is
|
||||
first seen). 3 re-baselines with no `/sample` chunk ⇒ Degraded with `LastSuccessfulRead` still advancing (no
|
||||
watchdog reconnect loop over a `SampleCount` ≥ `bufferSize`). Every parse prohibits DTDs; the streams walk is
|
||||
watchdog reconnect loop over a `SampleCount` > `bufferSize` — live: `count == bufferSize` is accepted; Test Connect + init warn, #825). Every parse prohibits DTDs; the streams walk is
|
||||
depth-bounded (64 — an unbounded recursion is a process-killing `StackOverflowException`). Non-finite floats are
|
||||
never Good; integer-valued decimals serve integral tags losslessly. Matrix: `docs/drivers/MTConnect.md`.
|
||||
- **MTConnect CONDITION alarms (#626):** a CONDITION raw tag is a native Part 9 condition only when its `TagConfig`
|
||||
|
||||
@@ -362,7 +362,7 @@ what the driver's cursor expects) is detected **two ways**, both triggering a
|
||||
**Re-baselines without progress are bounded *and visible* (#804).** The reconnect
|
||||
backoff already paced a stream that only ever re-baselines (`A_rebaseline_that_delivers_nothing…`),
|
||||
but every `/current` also marked the stream delivered, so the driver read **Healthy**
|
||||
for ever while no `/sample` chunk arrived — the live case is a `SampleCount` at or
|
||||
for ever while no `/sample` chunk arrived — the live case is a `SampleCount`
|
||||
above the Agent's `bufferSize`, refused `OUT_OF_RANGE` (`'count' must be less than N`)
|
||||
on every open. After **3** consecutive re-baselines with no chunk in between
|
||||
(`MaxRebaselinesWithoutProgress`, the Java driver's `MAX_REBASELINES_WITHOUT_PROGRESS`)
|
||||
@@ -371,11 +371,12 @@ observations in between"` and one Warning naming the `bufferSize` cause; it stay
|
||||
Degraded across further re-baselines until a chunk is delivered. Values keep flowing
|
||||
off the re-baselines and `LastSuccessfulRead` keeps advancing — each `/current` is the
|
||||
Agent answering — so the comms-loss watchdog does not reconnect-loop a configuration
|
||||
problem. Size `SampleCount` below the Agent's `bufferSize` (in the `/probe` header).
|
||||
problem. Size `SampleCount` at most the Agent's `bufferSize` (in the `/probe` header).
|
||||
**It is caught before it happens (#825):** the `/probe` header's `bufferSize` is parsed
|
||||
(`MTConnectProbeModel.BufferSize`, advisory — missing/malformed is null, never a parse failure); **Test
|
||||
Connect** reads `sampleCount` (default 1000 when absent) and answers Ok **with a warning** (an amber
|
||||
"OK with warning" chip showing the text) when it is at or above the buffer, and the driver's **initialize**
|
||||
"OK with warning" chip showing the text) when it is **above** the buffer (live, cppagent 2.7: `count == bufferSize` is
|
||||
accepted, `bufferSize + 1` refused `'count' must be less than {bufferSize+1}`), and the driver's **initialize**
|
||||
(every deploy) logs the same Warning and puts `agentBufferSize` on the `/hosts` diagnostics bag.
|
||||
|
||||
**An Agent `instanceId` change means the Agent restarted**, and is checked
|
||||
|
||||
@@ -61,7 +61,8 @@ public sealed partial class MTConnectDriver : IDriverDiagnostics
|
||||
/// <summary>
|
||||
/// lmxopcua#825 — the configuration warning for a <see cref="MTConnectDriverOptions.SampleCount"/> the Agent
|
||||
/// will refuse. An Agent answers <c>/sample?count=N</c> with <c>OUT_OF_RANGE</c> ("'count' must be less than
|
||||
/// bufferSize") when <c>N ≥ bufferSize</c>, so such a driver never streams: it re-baselines from
|
||||
/// bufferSize + 1") when <c>N > bufferSize</c> — live-measured on cppagent 2.7 (bufferSize 16384: 16384 is
|
||||
/// accepted, 16385 refused) — so such a driver never streams: it re-baselines from
|
||||
/// <c>/current</c> and reports Degraded after three of them. Shared by <see cref="MTConnectDriverProbe"/>
|
||||
/// (Test Connect) and the driver's initialize (deploy), so both say the same thing.
|
||||
/// </summary>
|
||||
@@ -69,10 +70,10 @@ public sealed partial class MTConnectDriver : IDriverDiagnostics
|
||||
/// <param name="bufferSize">The Agent's buffer size, or null when its header did not say.</param>
|
||||
/// <returns>The warning text, or <see langword="null"/> when the count fits (or the buffer size is unknown).</returns>
|
||||
internal static string? SampleCountBufferWarning(int sampleCount, long? bufferSize) =>
|
||||
bufferSize is { } size && sampleCount >= size
|
||||
? $"SampleCount {sampleCount.ToString(CultureInfo.InvariantCulture)} is at or above the Agent's bufferSize "
|
||||
bufferSize is { } size && sampleCount > size
|
||||
? $"SampleCount {sampleCount.ToString(CultureInfo.InvariantCulture)} is above the Agent's bufferSize "
|
||||
+ $"{size.ToString(CultureInfo.InvariantCulture)}: the Agent refuses every /sample stream (OUT_OF_RANGE), so "
|
||||
+ "values will only re-baseline from /current and the driver reports Degraded. Set SampleCount below "
|
||||
+ "values will only re-baseline from /current and the driver reports Degraded. Set SampleCount to at most "
|
||||
+ $"{size.ToString(CultureInfo.InvariantCulture)}."
|
||||
: null;
|
||||
|
||||
|
||||
@@ -2350,7 +2350,7 @@ public sealed partial class MTConnectDriver
|
||||
|
||||
// #804 — the backoff above PACES a stream that only ever re-baselines, but each /current
|
||||
// also marked the stream delivered, so it read Healthy for ever while no /sample chunk
|
||||
// arrived (a SampleCount at or above the Agent's bufferSize is refused OUT_OF_RANGE on
|
||||
// arrived (a SampleCount above the Agent's bufferSize is refused OUT_OF_RANGE on
|
||||
// EVERY open). After MaxRebaselinesWithoutProgress in a row it is reported Degraded, and
|
||||
// stays so (holdStalled) until a chunk is delivered.
|
||||
if (!outcome.DeliveredAny &&
|
||||
@@ -2363,7 +2363,7 @@ public sealed partial class MTConnectDriver
|
||||
|
||||
_logger.LogWarning(
|
||||
outcome.Error,
|
||||
"MTConnect driver {DriverInstanceId}: {Count} consecutive /current re-baselines of {AgentUri} without a single /sample chunk in between; reporting the stream Degraded until one is delivered. Values are re-baselined, not streamed. An Agent answering every /sample with OUT_OF_RANGE usually means SampleCount is at or above its bufferSize.",
|
||||
"MTConnect driver {DriverInstanceId}: {Count} consecutive /current re-baselines of {AgentUri} without a single /sample chunk in between; reporting the stream Degraded until one is delivered. Values are re-baselined, not streamed. An Agent answering every /sample with OUT_OF_RANGE usually means SampleCount is above its bufferSize.",
|
||||
_driverInstanceId, rebaselinesWithoutProgress, AgentDisplay(options));
|
||||
}
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ public sealed class MTConnectDriverProbe : IDriverProbe
|
||||
|
||||
/// <summary>
|
||||
/// The <c>/sample</c> <c>count</c> (lmxopcua#825). Not needed to reach the Agent, but read so Test Connect
|
||||
/// can warn when it is at or above the Agent's <c>bufferSize</c> — a config the Agent will refuse on every
|
||||
/// can warn when it is above the Agent's <c>bufferSize</c> — a config the Agent will refuse on every
|
||||
/// stream. Absent ⇒ the driver's own default.
|
||||
/// </summary>
|
||||
public int? SampleCount { get; set; }
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect;
|
||||
/// <param name="BufferSize">
|
||||
/// The Agent's observation ring-buffer size (the <c>/probe</c> <c>Header</c>'s <c>bufferSize</c>), or
|
||||
/// <see langword="null"/> when the header omits it or it is not a positive integer. A <c>/sample</c>
|
||||
/// <c>count</c> at or above it is refused <c>OUT_OF_RANGE</c> by the Agent (lmxopcua#825).
|
||||
/// <c>count</c> above it is refused <c>OUT_OF_RANGE</c> by the Agent (lmxopcua#825; live: <c>count == bufferSize</c> is accepted).
|
||||
/// </param>
|
||||
public sealed record MTConnectProbeModel(IReadOnlyList<MTConnectDevice> Devices, long? BufferSize = null);
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ internal static class MTConnectXml
|
||||
/// <remarks>
|
||||
/// <b>Why this exists (#804).</b> A modern Agent (cppagent 2.x, the fixture's
|
||||
/// <c>mtconnect/agent:2.7</c>) does not serve its error documents under HTTP 200 the way older
|
||||
/// ones did: it answers an out-of-range <c>/sample?from=</c> — and a <c>count</c> at or above its
|
||||
/// ones did: it answers an out-of-range <c>/sample?from=</c> — and a <c>count</c> above its
|
||||
/// buffer — with <b>HTTP 400</b>, and an unknown device with <b>404</b>, each carrying a
|
||||
/// well-formed <c>MTConnectError</c> body. A status check alone turns those into an
|
||||
/// <see cref="HttpRequestException"/>, which the pump treats as a transient transport loss and
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
{
|
||||
@if (_result.Ok && WarningOf(_result.Message) is { } warning)
|
||||
{
|
||||
@* Reachable but misconfigured (e.g. MTConnect SampleCount >= the Agent's bufferSize, #825): a probe
|
||||
@* Reachable but misconfigured (e.g. MTConnect SampleCount > the Agent's bufferSize, #825): a probe
|
||||
that answers Ok with a "WARNING:" message is shown amber with the warning, never a plain green OK. *@
|
||||
<span class="chip chip-warn" title="@warning">
|
||||
OK with warning · @_result.LatencyMs?.ToString("F0") ms
|
||||
|
||||
+14
-5
@@ -868,7 +868,7 @@ public sealed class MTConnectAgentIntegrationTests(MTConnectAgentFixture agent)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #804 — a <c>SampleCount</c> at or above the Agent's <c>bufferSize</c> is refused <c>OUT_OF_RANGE</c> on
|
||||
/// #804 — a <c>SampleCount</c> above the Agent's <c>bufferSize</c> is refused <c>OUT_OF_RANGE</c> on
|
||||
/// EVERY <c>/sample</c> open (live: <c>'count' must be less than N</c>). The driver keeps serving values off
|
||||
/// <c>/current</c> re-baselines, and — after three in a row with no <c>/sample</c> chunk — reports the stream
|
||||
/// Degraded with <c>LastSuccessfulRead</c> still advancing, rather than Healthy for ever (before #804 the
|
||||
@@ -949,7 +949,8 @@ public sealed class MTConnectAgentIntegrationTests(MTConnectAgentFixture agent)
|
||||
|
||||
/// <summary>
|
||||
/// #825 — Test Connect reads the live Agent's <c>/probe</c> <c>bufferSize</c> and warns (still Ok) when the
|
||||
/// configured <c>SampleCount</c> would be refused; one below the buffer is the falsifiability control.
|
||||
/// configured <c>SampleCount</c> would be refused. The live Agent accepts <c>count == bufferSize</c> and refuses
|
||||
/// <c>bufferSize + 1</c>, so both sides of that edge are checked against the Agent itself as well as the probe.
|
||||
/// </summary>
|
||||
[Fact(Timeout = TestTimeoutMs)]
|
||||
public async Task Test_Connect_warns_when_SampleCount_reaches_the_live_agents_buffer_size()
|
||||
@@ -963,14 +964,22 @@ public sealed class MTConnectAgentIntegrationTests(MTConnectAgentFixture agent)
|
||||
Assert.Skip($"The Agent at {agent.AgentUri} declares no usable bufferSize in its /probe header.");
|
||||
}
|
||||
|
||||
// The Agent's own edge, so the warning threshold is pinned to what the Agent actually does.
|
||||
using (var http = new HttpClient { Timeout = TimeSpan.FromSeconds(15) })
|
||||
{
|
||||
(await http.GetStringAsync($"{agent.AgentUri}/sample?count={bufferSize.Value}", Ct)).ShouldNotContain("OUT_OF_RANGE");
|
||||
var over = await http.GetAsync($"{agent.AgentUri}/sample?count={bufferSize.Value + 1}", Ct);
|
||||
(await over.Content.ReadAsStringAsync(Ct)).ShouldContain("OUT_OF_RANGE");
|
||||
}
|
||||
|
||||
var refused = await new MTConnectDriverProbe().ProbeAsync(
|
||||
JsonSerializer.Serialize(new { agentUri = agent.AgentUri, sampleCount = bufferSize.Value }),
|
||||
JsonSerializer.Serialize(new { agentUri = agent.AgentUri, sampleCount = bufferSize.Value + 1 }),
|
||||
TimeSpan.FromSeconds(15), Ct);
|
||||
refused.Ok.ShouldBeTrue(refused.Message);
|
||||
refused.Message!.ShouldContain($"WARNING: SampleCount {bufferSize.Value} is at or above the Agent's bufferSize {bufferSize.Value}");
|
||||
refused.Message!.ShouldContain($"WARNING: SampleCount {bufferSize.Value + 1} is above the Agent's bufferSize {bufferSize.Value}");
|
||||
|
||||
var fits = await new MTConnectDriverProbe().ProbeAsync(
|
||||
JsonSerializer.Serialize(new { agentUri = agent.AgentUri, sampleCount = bufferSize.Value - 1 }),
|
||||
JsonSerializer.Serialize(new { agentUri = agent.AgentUri, sampleCount = bufferSize.Value }),
|
||||
TimeSpan.FromSeconds(15), Ct);
|
||||
fits.Ok.ShouldBeTrue(fits.Message);
|
||||
fits.Message!.ShouldNotContain("WARNING");
|
||||
|
||||
+10
-9
@@ -7,7 +7,7 @@ using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// lmxopcua#825 — a <c>SampleCount</c> at or above the Agent's <c>bufferSize</c> is refused <c>OUT_OF_RANGE</c> on
|
||||
/// lmxopcua#825 — a <c>SampleCount</c> above the Agent's <c>bufferSize</c> is refused <c>OUT_OF_RANGE</c> on
|
||||
/// every <c>/sample</c> stream. The <c>/probe</c> header carries <c>bufferSize</c>, so Test Connect and the
|
||||
/// driver's initialize (deploy) both warn instead of leaving it to surface as a Degraded stream.
|
||||
/// </summary>
|
||||
@@ -37,15 +37,15 @@ public sealed class MTConnectBufferSizeWarningTests
|
||||
MTConnectProbeParser.Parse(ProbeXml(attribute)).BufferSize.ShouldBe(expected);
|
||||
|
||||
[Theory]
|
||||
[InlineData(16383, 16384L, false)]
|
||||
[InlineData(16384, 16384L, true)]
|
||||
[InlineData(16384, 16384L, false)] // live, cppagent 2.7: count == bufferSize is accepted
|
||||
[InlineData(16385, 16384L, true)] // ... and bufferSize + 1 refused OUT_OF_RANGE
|
||||
[InlineData(20000, 16384L, true)]
|
||||
[InlineData(1_000_000, null, false)]
|
||||
public void The_warning_fires_exactly_at_and_above_the_buffer_size(int sampleCount, long? bufferSize, bool warns)
|
||||
public void The_warning_fires_only_above_the_buffer_size(int sampleCount, long? bufferSize, bool warns)
|
||||
{
|
||||
var warning = MTConnectDriver.SampleCountBufferWarning(sampleCount, bufferSize);
|
||||
(warning is not null).ShouldBe(warns);
|
||||
if (warns) warning!.ShouldContain($"below {bufferSize}");
|
||||
if (warns) warning!.ShouldContain($"at most {bufferSize}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -53,7 +53,7 @@ public sealed class MTConnectBufferSizeWarningTests
|
||||
{
|
||||
var probe = new MTConnectDriverProbe(new StubHandler(ProbeXml("16384")));
|
||||
var r = await probe.ProbeAsync(
|
||||
"{\"agentUri\":\"http://stub-agent:5000\",\"sampleCount\":16384}", TimeSpan.FromSeconds(3),
|
||||
"{\"agentUri\":\"http://stub-agent:5000\",\"sampleCount\":16385}", TimeSpan.FromSeconds(3),
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
r.Ok.ShouldBeTrue("the Agent answered — reachability is not the problem");
|
||||
@@ -70,7 +70,7 @@ public sealed class MTConnectBufferSizeWarningTests
|
||||
"{\"agentUri\":\"http://stub-agent:5000\"}", TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
|
||||
|
||||
r.Ok.ShouldBeTrue();
|
||||
r.Message!.ShouldContain("SampleCount 1000 is at or above the Agent's bufferSize 512");
|
||||
r.Message!.ShouldContain("SampleCount 1000 is above the Agent's bufferSize 512");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -78,7 +78,7 @@ public sealed class MTConnectBufferSizeWarningTests
|
||||
{
|
||||
var probe = new MTConnectDriverProbe(new StubHandler(ProbeXml("16384")));
|
||||
var r = await probe.ProbeAsync(
|
||||
"{\"agentUri\":\"http://stub-agent:5000\",\"sampleCount\":1000}", TimeSpan.FromSeconds(3),
|
||||
"{\"agentUri\":\"http://stub-agent:5000\",\"sampleCount\":16384}", TimeSpan.FromSeconds(3),
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
r.Ok.ShouldBeTrue();
|
||||
@@ -86,7 +86,8 @@ public sealed class MTConnectBufferSizeWarningTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(131072, true)]
|
||||
[InlineData(131073, true)]
|
||||
[InlineData(131072, false)]
|
||||
[InlineData(1000, false)]
|
||||
public async Task Initialize_warns_and_surfaces_the_buffer_size_on_the_diagnostics_bag(int sampleCount, bool warns)
|
||||
{
|
||||
|
||||
@@ -1168,7 +1168,7 @@ public sealed class MTConnectSubscribeTests
|
||||
|
||||
/// <summary>
|
||||
/// <b>#804 (Java-driver parity: <c>MAX_REBASELINES_WITHOUT_PROGRESS</c>).</b> An Agent that answers EVERY
|
||||
/// <c>/sample</c> open with <c>OUT_OF_RANGE</c> (live: a <c>count</c> at or above its <c>bufferSize</c>) is
|
||||
/// <c>/sample</c> open with <c>OUT_OF_RANGE</c> (live: a <c>count</c> above its <c>bufferSize</c>) is
|
||||
/// re-baselined from <c>/current</c> each time, and the backoff paces that — but every re-baseline also marked
|
||||
/// the stream delivered, so the driver read <b>Healthy</b> for ever while not one <c>/sample</c> chunk arrived.
|
||||
/// After <see cref="MTConnectDriver.MaxRebaselinesWithoutProgress"/> in a row it is Degraded and stays so
|
||||
|
||||
Reference in New Issue
Block a user