SendEvent over gRPC: implement + live-validate (was capture-gated)
Captured the native 2023 R2 client's gRPC event send (new capture-send-event harness scenario): it rides HistoryService.AddStreamValues with the SAME "OS" (0x534F) storage-sample buffer the WCF path already uses (HistorianEventWrite- Protocol) — confirming "no distinct RPC", and that it is NOT the historical write's "ON" buffer. Diffed the write-enabled vs read-only v8 Event open: byte- identical apart from per-session crypto, so the existing OpenSession event path is reused unchanged. So SendEvent-over-gRPC was pure assembly of proven parts: - HistorianGrpcEventWriteOrchestrator = v8 Event open + CM_EVENT registration (UpdC3/RegisterTags/EnsureTags) + AddStreamValues(OS buffer). - HistorianClient.SendEventAsync now routes to it for RemoteGrpc (WCF otherwise). Live-validated end-to-end against the 2023 R2 server: pure-managed SDK send → AddStreamValues BSuccess=true → the event reads back from the server (markers confirmed in returned event rows). The native gRPC RegisterTags(24B) + EnsureTags(86B) byte-match our serializers (new GrpcEventSendProtocolTests golden, closing the 83-vs-86 EnsureTags question). Gated live test SendEventAsync_OverGrpc_AcceptsEvent (opt-in HISTORIAN_GRPC_EVENT_SEND=1). 331 offline tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using AVEVA.Historian.Client.Wcf;
|
||||
using Xunit;
|
||||
|
||||
namespace AVEVA.Historian.Client.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Golden-byte coverage for the 2023 R2 gRPC event-SEND registration buffers, pinned against a live
|
||||
/// native capture (<c>capture-send-event</c> scenario, 2026-06-23). The send itself rides
|
||||
/// <c>HistoryService.AddStreamValues</c> with the same "OS" buffer the WCF path uses
|
||||
/// (<see cref="HistorianEventWriteProtocol"/>, already golden-tested in
|
||||
/// <c>WcfEventWriteProtocolTests</c>); what is gRPC-specific is the CM_EVENT registration the event
|
||||
/// connection performs first (RegisterTags + the 86-byte gRPC EnsureTags). These fixtures are the raw
|
||||
/// bytes the native client sent on the wire — they carry no identity (CM_EVENT / "AnE Event" /
|
||||
/// constant tag + event-type GUIDs / a registration FILETIME).
|
||||
/// </summary>
|
||||
public class GrpcEventSendProtocolTests
|
||||
{
|
||||
// GrpcHistoryClient.RegisterTags.tagInfos captured from the native event connection: the packet
|
||||
// header 50 67 02 00 + count(1) + the 16-byte CM_EVENT tag GUID.
|
||||
private const string CapturedRegisterTagsHex =
|
||||
"506702000100000045813b35f05d464da253871aef49b321";
|
||||
|
||||
// GrpcHistoryClient.EnsureTags.tagInfos captured from the native event connection (86 bytes): the
|
||||
// 8-byte EnsureTags header + CM_EVENT CTagMetadata + a registration FILETIME + the …e01f2f27
|
||||
// event-type GUID.
|
||||
private const string CapturedEnsureTagsHex =
|
||||
"4e670300010000000386000545813b35f05d464da253871aef49b321090800434d5f4556454e54090900416e45204576656e7402020100000001000000004e18d6bd4503dd0142ae595fb63b604791a5ab0be01f2f27";
|
||||
|
||||
[Fact]
|
||||
public void BuildRegisterCmEventInputBuffer_MatchesNativeGrpcCapture()
|
||||
{
|
||||
byte[] expected = Convert.FromHexString(CapturedRegisterTagsHex);
|
||||
byte[] actual = HistorianEventRegistrationProtocol.BuildRegisterCmEventInputBuffer();
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializeCmEventEnsureTagsGrpc_MatchesNativeGrpcCapture()
|
||||
{
|
||||
byte[] expected = Convert.FromHexString(CapturedEnsureTagsHex);
|
||||
Assert.Equal(86, expected.Length);
|
||||
|
||||
// The only run-varying field is the registration FILETIME (the 8 bytes immediately before the
|
||||
// trailing 16-byte event-type GUID). Feed the captured time back so the comparison is exact.
|
||||
long filetime = BinaryPrimitives.ReadInt64LittleEndian(expected.AsSpan(expected.Length - 24, 8));
|
||||
DateTime createdUtc = DateTime.FromFileTimeUtc(filetime);
|
||||
|
||||
byte[] actual = HistorianAddTagsProtocol.SerializeCmEventEnsureTagsGrpc(createdUtc);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
@@ -233,6 +233,37 @@ public sealed class HistorianGrpcIntegrationTests
|
||||
Assert.Contains(samples, s => s.NumericValue is { } v && Math.Abs(v - expected) < 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendEventAsync_OverGrpc_AcceptsEvent()
|
||||
{
|
||||
string? host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST");
|
||||
// Gated additionally on a dedicated opt-in so this WRITE test never runs by accident — it
|
||||
// appends a clearly-marked test event to the server's event history. Captured 2026-06-23:
|
||||
// the gRPC event send rides HistoryService.AddStreamValues with the same "OS" buffer the WCF
|
||||
// path uses (HistorianEventWriteProtocol), on a v8 Event session + CM_EVENT registration.
|
||||
if (string.IsNullOrWhiteSpace(host)
|
||||
|| string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HISTORIAN_USER"))
|
||||
|| Environment.GetEnvironmentVariable("HISTORIAN_GRPC_EVENT_SEND") is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HistorianClient client = new(BuildOptions(host));
|
||||
|
||||
var evt = new HistorianEvent(
|
||||
Id: Guid.NewGuid(),
|
||||
EventTimeUtc: DateTime.UtcNow,
|
||||
ReceivedTimeUtc: DateTime.UtcNow,
|
||||
Type: "SdkSendProbe",
|
||||
SourceName: "SdkSendProbe",
|
||||
Namespace: "SdkCapture",
|
||||
RevisionVersion: 0,
|
||||
Properties: new Dictionary<string, object?> { ["SdkProbeProp"] = "SdkProbeValue" });
|
||||
|
||||
bool sent = await client.SendEventAsync(evt, CancellationToken.None);
|
||||
Assert.True(sent, "gRPC SendEvent should be accepted by the server (AddStreamValues BSuccess).");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAggregateAsync_OverGrpc_ReturnsTimeWeightedAverageRows()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user