6dedf5158f
Review 2026-10-21 G-5, G-6, G-8 ("wire them all").
- G-5: GatewayGalaxyHierarchySource now pages the DiscoverHierarchy RPC
itself (DiscoverHierarchyRawAsync) at Repository.DiscoverPageSize. The
mxaccessgw client's typed DiscoverHierarchyAsync overloads hard-code a
5000-row page and DiscoverHierarchyOptions carries no page size, so the
authored knob reached nothing; the raw request takes page_size on the
wire, so it is honoured here (non-positive → 5000). The loop keeps the
client's repeated-page-token guard.
- G-6: GalaxyDriverProbe reads ProbeTimeoutSeconds from the config and
runs under min(caller budget, configured cap) — the knob can shorten a
probe, never extend it past the host's budget. The TCP preflight now
shares that deadline instead of hanging on the caller's token alone.
- G-8: GatewayGalaxyDataWriter runs a batch as one lane per distinct tag,
concurrently and bounded at 8 (MaxConcurrentWriteLanes); same-tag
writes keep their authored order inside one lane and results land at
their request's index. Acks stay serial by design.
- GalaxyDriverForm: help text now says what the two knobs actually do.
Refs #735
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
70 lines
3.1 KiB
C#
70 lines
3.1 KiB
C#
using System.Diagnostics;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests;
|
|
|
|
/// <summary>
|
|
/// Review 2026-10-21 G-6: <c>ProbeTimeoutSeconds</c> was authored and compared for equivalence but
|
|
/// never consumed — the probe ran on the caller's budget alone. It now caps the probe deadline
|
|
/// (it can shorten the caller's budget, never extend it).
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class GalaxyDriverProbeTimeoutTests
|
|
{
|
|
/// <summary>The effective deadline is the shorter of the caller's budget and the configured cap.</summary>
|
|
[Theory]
|
|
[InlineData(10, 30, 10)] // caller shorter → caller wins
|
|
[InlineData(10, 2, 2)] // config shorter → config caps
|
|
[InlineData(10, 0, 10)] // unset config → caller
|
|
[InlineData(10, -1, 10)] // nonsense config → caller
|
|
[InlineData(0, 5, 1)] // no caller budget → 1 s floor beats a longer config
|
|
[InlineData(0, 0, 1)] // nothing → 1 s floor
|
|
public void EffectiveTimeout_is_the_shorter_of_caller_and_config(int callerSeconds, int configSeconds, int expectedSeconds)
|
|
=> GalaxyDriverProbe.EffectiveTimeout(TimeSpan.FromSeconds(callerSeconds), configSeconds)
|
|
.ShouldBe(TimeSpan.FromSeconds(expectedSeconds));
|
|
|
|
/// <summary>
|
|
/// Against a port that accepts TCP but never speaks gRPC, a 1 s configured cap returns long
|
|
/// before the caller's 20 s budget. The gRPC handshake itself does the waiting here, so this
|
|
/// exercises the real deadline path, not the pure helper.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Configured_cap_shortens_the_probe_against_a_silent_listener()
|
|
{
|
|
var listener = new TcpListener(IPAddress.Loopback, 0);
|
|
listener.Start();
|
|
try
|
|
{
|
|
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
|
var config = $$"""
|
|
{
|
|
"Gateway": { "Endpoint": "http://127.0.0.1:{{port}}", "ApiKeySecretRef": "dev:k", "UseTls": false },
|
|
"MxAccess": { "ClientName": "probe-timeout" },
|
|
"Repository": {},
|
|
"Reconnect": {},
|
|
"ProbeTimeoutSeconds": 1
|
|
}
|
|
""";
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
var result = await new GalaxyDriverProbe().ProbeAsync(config, TimeSpan.FromSeconds(20), TestContext.Current.CancellationToken);
|
|
sw.Stop();
|
|
|
|
result.Ok.ShouldBeFalse();
|
|
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(10), $"the 1 s configured cap must bound the probe (message: {result.Message})");
|
|
result.Message.ShouldNotBeNull();
|
|
(result.Message!.Contains("timed out", StringComparison.OrdinalIgnoreCase)
|
|
|| result.Message.Contains("DeadlineExceeded", StringComparison.OrdinalIgnoreCase)
|
|
|| result.Message.Contains("handshake failed", StringComparison.OrdinalIgnoreCase))
|
|
.ShouldBeTrue($"unexpected message: '{result.Message}'");
|
|
}
|
|
finally
|
|
{
|
|
listener.Stop();
|
|
}
|
|
}
|
|
}
|