perf(mtconnect): headers-only connectivity probe (R06:T-7)
The connectivity probe parsed the full /probe device model every 5 s and discarded it. It now calls a new IMTConnectAgentClient.PingAsync: one GET /probe read with ResponseHeadersRead and a status check; the body is dropped unread. Same failure surface as ProbeAsync (the default implementation delegates to it). The probe-tick tests now count pings and pin that no tick issues a full /probe; the shutdown test's absence assertion gained a positive control. Refs #743 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -76,6 +76,17 @@ public interface IMTConnectAgentClient : IDisposable
|
||||
/// <returns>The parsed device hierarchy.</returns>
|
||||
Task<MTConnectProbeModel> ProbeAsync(CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// A connectivity check: does the Agent answer <c>/probe</c> with a success status? Reads the response
|
||||
/// headers only — the device model is neither downloaded in full nor parsed (R06:T-7: the connectivity
|
||||
/// probe used to parse the whole <c>/probe</c> document every tick and throw it away). Throws exactly when
|
||||
/// <see cref="ProbeAsync"/> would for an unreachable or refusing Agent. The default delegates to
|
||||
/// <see cref="ProbeAsync"/> and discards the model.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token (the caller's deadline).</param>
|
||||
/// <returns>A task completing when the Agent has answered with a success status.</returns>
|
||||
async Task PingAsync(CancellationToken ct) => _ = await ProbeAsync(ct).ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Issues an Agent <c>/current</c> request and returns the latest observed value for every
|
||||
/// data item. Used both to prime the observation index at startup and to re-baseline after
|
||||
|
||||
@@ -182,6 +182,35 @@ public sealed class MTConnectAgentClient : IMTConnectAgentClient, IDisposable
|
||||
return MTConnectProbeParser.Parse(body);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task PingAsync(CancellationToken ct)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this);
|
||||
|
||||
using var lifetime = CancellationTokenSource.CreateLinkedTokenSource(ct, _disposeCts.Token);
|
||||
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token);
|
||||
deadline.CancelAfter(_requestTimeout);
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, _probeUri);
|
||||
HttpResponseMessage response;
|
||||
try
|
||||
{
|
||||
// ResponseHeadersRead: the status line is the whole answer this needs. The body (the device model —
|
||||
// the largest document the Agent serves) is never buffered or parsed; disposing the response drops it.
|
||||
response = await _http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, deadline.Token)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (!lifetime.IsCancellationRequested)
|
||||
{
|
||||
throw TimedOut(_probeUri);
|
||||
}
|
||||
|
||||
using (response)
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<MTConnectStreamsResult> CurrentAsync(CancellationToken ct)
|
||||
{
|
||||
|
||||
@@ -1632,9 +1632,9 @@ public sealed partial class MTConnectDriver
|
||||
/// free, but it only exists while something is subscribed — an instance with no
|
||||
/// subscriptions would report <see cref="HostState.Unknown"/> indefinitely, and host
|
||||
/// reachability would become a function of OPC UA client behaviour. A dedicated tick is
|
||||
/// the one signal that is true whether or not anyone is watching. Its cost is a parse of
|
||||
/// the device model per interval, which is why <see cref="MTConnectProbeOptions.Interval"/>
|
||||
/// exists.
|
||||
/// the one signal that is true whether or not anyone is watching. Its cost used to be a parse
|
||||
/// of the whole device model per interval; it is now one request whose response headers are
|
||||
/// read and whose body is dropped (<see cref="IMTConnectAgentClient.PingAsync"/>, R06:T-7).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>It waits first, then probes.</b> <see cref="StartCoreAsync"/> has just completed a
|
||||
@@ -1674,8 +1674,8 @@ public sealed partial class MTConnectDriver
|
||||
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
deadline.CancelAfter(options.Probe.Timeout);
|
||||
|
||||
// The answer is deliberately discarded — see the remarks.
|
||||
_ = await client.ProbeAsync(deadline.Token).ConfigureAwait(false);
|
||||
// Headers only — the device model is neither downloaded in full nor parsed (R06:T-7).
|
||||
await client.PingAsync(deadline.Token).ConfigureAwait(false);
|
||||
reachable = true;
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
|
||||
Reference in New Issue
Block a user