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 Grpc.Core;
|
||||
using AVEVA.Historian.Client.Grpc;
|
||||
using AVEVA.Historian.Client.Models;
|
||||
using Xunit;
|
||||
@@ -104,7 +105,6 @@ public sealed class EventSessionReuseSpikeTests
|
||||
if (!TryGetEnv(out string host, out string sandboxTag)) return;
|
||||
HistorianClientOptions options = BuildOptions(host);
|
||||
var writeOrch = new HistorianGrpcEventWriteOrchestrator(options);
|
||||
var readOrch = new HistorianGrpcEventOrchestrator(options);
|
||||
|
||||
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options);
|
||||
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);
|
||||
_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();
|
||||
try
|
||||
{
|
||||
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)");
|
||||
}
|
||||
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)
|
||||
{
|
||||
// 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)");
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
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? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD");
|
||||
@@ -212,9 +232,10 @@ public sealed class EventSessionReuseSpikeTests
|
||||
? parsed
|
||||
: HistorianClientOptions.DefaultGrpcPort;
|
||||
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)
|
||||
: new HistorianClientOptions { Host = host }.RequestTimeout;
|
||||
: new HistorianClientOptions { Host = host }.RequestTimeout);
|
||||
|
||||
return new HistorianClientOptions
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user