test(spike): bound event read-after-send so the spike can't hang on C2 long-poll
The Q3 read-after-send probe (ReusedEventSession_ServesReadAfterSend_BestEffort) long-polled GetNext to the no-data terminal with no tight bound and ran past the 5-min suite timeout on the live run. Bound it two ways: a read-only options copy with a 5s RequestTimeout (so each GetNext RPC deadlines fast) and an 8s CancellationToken passed as ct. Either fuse returns the method in ~10s; the timeout/cancellation is logged as the expected C2-gated outcome (still no assert). Other three spike methods unchanged. Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using Grpc.Core;
|
||||||
using AVEVA.Historian.Client.Grpc;
|
using AVEVA.Historian.Client.Grpc;
|
||||||
using AVEVA.Historian.Client.Models;
|
using AVEVA.Historian.Client.Models;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -104,7 +105,6 @@ public sealed class EventSessionReuseSpikeTests
|
|||||||
if (!TryGetEnv(out string host, out string sandboxTag)) return;
|
if (!TryGetEnv(out string host, out string sandboxTag)) return;
|
||||||
HistorianClientOptions options = BuildOptions(host);
|
HistorianClientOptions options = BuildOptions(host);
|
||||||
var writeOrch = new HistorianGrpcEventWriteOrchestrator(options);
|
var writeOrch = new HistorianGrpcEventWriteOrchestrator(options);
|
||||||
var readOrch = new HistorianGrpcEventOrchestrator(options);
|
|
||||||
|
|
||||||
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options);
|
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options);
|
||||||
HistorianGrpcHandshake.Session session = writeOrch.OpenAndRegisterEventSession(connection, CancellationToken.None);
|
HistorianGrpcHandshake.Session session = writeOrch.OpenAndRegisterEventSession(connection, CancellationToken.None);
|
||||||
@@ -112,16 +112,34 @@ public sealed class EventSessionReuseSpikeTests
|
|||||||
bool sent = writeOrch.SendEventOnSession(connection, session, BuildSandboxEvent(sandboxTag, attempt: 0), CancellationToken.None);
|
bool sent = writeOrch.SendEventOnSession(connection, session, BuildSandboxEvent(sandboxTag, attempt: 0), CancellationToken.None);
|
||||||
_output.WriteLine($"seed-send before read-probe ok={sent}");
|
_output.WriteLine($"seed-send before read-probe ok={sent}");
|
||||||
|
|
||||||
|
// HARD bound so this probe CANNOT hang on the known C2 event-read long-poll (GetNext blocks to the
|
||||||
|
// no-data terminal on an idle box). Two independent fuses: (1) a read-only options copy with a 5s
|
||||||
|
// RequestTimeout so each underlying GetNext RPC deadlines quickly (the read orchestrator caps its
|
||||||
|
// poll deadline at min(10s, RequestTimeout)); (2) an 8s CancellationToken passed as ct so the chain
|
||||||
|
// is cancelled even if a per-RPC deadline is not honored over a tunnel. Whichever fires first, the
|
||||||
|
// method returns in ~10s max even when the read never returns data.
|
||||||
|
HistorianClientOptions readOptions = BuildOptions(host, requestTimeoutOverride: TimeSpan.FromSeconds(5));
|
||||||
|
var readOrch = new HistorianGrpcEventOrchestrator(readOptions);
|
||||||
|
using var readCts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
||||||
|
|
||||||
(DateTime startUtc, DateTime endUtc) = LastSevenDays();
|
(DateTime startUtc, DateTime endUtc) = LastSevenDays();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
List<HistorianEvent> rows = readOrch.RunEventQueryOnSession(
|
List<HistorianEvent> rows = readOrch.RunEventQueryOnSession(
|
||||||
connection, session, startUtc, endUtc, filter: null, CancellationToken.None);
|
connection, session, startUtc, endUtc, filter: null, readCts.Token);
|
||||||
_output.WriteLine($"read-after-send -> OK (rows={rows.Count}) => ONE-KIND (an Event session serves send AND read)");
|
_output.WriteLine($"read-after-send -> OK (rows={rows.Count}) => ONE-KIND (an Event session serves send AND read)");
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) when (ex is OperationCanceledException
|
||||||
|
|| (ex is RpcException rpc && rpc.StatusCode == StatusCode.DeadlineExceeded))
|
||||||
|
{
|
||||||
|
// EXPECTED outcome: the read hit its bound (CTS timeout or per-RPC deadline) without returning —
|
||||||
|
// consistent with the C2 event-read gating (GetNext long-polls to the no-data terminal). This is
|
||||||
|
// the recorded one-kind finding, NOT a failure.
|
||||||
|
_output.WriteLine($"read-after-send did not return within the bound (consistent with C2 event-read gating): {ex.GetType().Name}");
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Reads are gated C2 — a rejection / long-poll terminal is itself the finding, NOT a failure.
|
// Any other rejection on the reused session is also the finding, not a failure.
|
||||||
_output.WriteLine($"read-after-send -> swallowed ({ex.GetType().Name}: {ex.Message}) => read gated/unverified over gRPC (expected)");
|
_output.WriteLine($"read-after-send -> swallowed ({ex.GetType().Name}: {ex.Message}) => read gated/unverified over gRPC (expected)");
|
||||||
}
|
}
|
||||||
// No assertion: this method's job is to RECORD the one-kind outcome for B0c, not gate on it.
|
// No assertion: this method's job is to RECORD the one-kind outcome for B0c, not gate on it.
|
||||||
@@ -203,7 +221,9 @@ public sealed class EventSessionReuseSpikeTests
|
|||||||
return (end - TimeSpan.FromDays(7), end);
|
return (end - TimeSpan.FromDays(7), end);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HistorianClientOptions BuildOptions(string host)
|
// requestTimeoutOverride: when set, forces RequestTimeout (used by the read-after-send probe to give
|
||||||
|
// each GetNext RPC a short deadline). null preserves the env-driven default for the send/idle methods.
|
||||||
|
private static HistorianClientOptions BuildOptions(string host, TimeSpan? requestTimeoutOverride = null)
|
||||||
{
|
{
|
||||||
string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER");
|
string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER");
|
||||||
string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD");
|
string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD");
|
||||||
@@ -212,9 +232,10 @@ public sealed class EventSessionReuseSpikeTests
|
|||||||
? parsed
|
? parsed
|
||||||
: HistorianClientOptions.DefaultGrpcPort;
|
: HistorianClientOptions.DefaultGrpcPort;
|
||||||
bool tls = string.Equals(Environment.GetEnvironmentVariable("HISTORIAN_GRPC_TLS"), "true", StringComparison.OrdinalIgnoreCase);
|
bool tls = string.Equals(Environment.GetEnvironmentVariable("HISTORIAN_GRPC_TLS"), "true", StringComparison.OrdinalIgnoreCase);
|
||||||
TimeSpan timeout = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_GRPC_TIMEOUT"), out int secs) && secs > 0
|
TimeSpan timeout = requestTimeoutOverride
|
||||||
|
?? (int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_GRPC_TIMEOUT"), out int secs) && secs > 0
|
||||||
? TimeSpan.FromSeconds(secs)
|
? TimeSpan.FromSeconds(secs)
|
||||||
: new HistorianClientOptions { Host = host }.RequestTimeout;
|
: new HistorianClientOptions { Host = host }.RequestTimeout);
|
||||||
|
|
||||||
return new HistorianClientOptions
|
return new HistorianClientOptions
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user