From 2f689cbe71fca030ce0a3799ab199f5c2a9cc4e2 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 25 Jun 2026 10:20:21 -0400 Subject: [PATCH] =?UTF-8?q?feat(grpc):=20=E2=80=A6OnSession=20seams=20on?= =?UTF-8?q?=20HistorianGrpcTagClient=20(browse=20+=20metadata)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DRY split mirroring HistorianGrpcReadOrchestrator.RunRawQueryOnSession: browse + GetTagInfos(metadata) gain externally-supplied connection+session seams; per-call wrappers delegate. Behaviour-preserving (pending.md A1 broadening). Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii --- .../Grpc/HistorianGrpcTagClient.cs | 49 +++++++++++++++++++ .../TagClientOnSessionSeamTests.cs | 22 +++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/AVEVA.Historian.Client.Tests/TagClientOnSessionSeamTests.cs diff --git a/src/AVEVA.Historian.Client/Grpc/HistorianGrpcTagClient.cs b/src/AVEVA.Historian.Client/Grpc/HistorianGrpcTagClient.cs index 79cd32e..945bde7 100644 --- a/src/AVEVA.Historian.Client/Grpc/HistorianGrpcTagClient.cs +++ b/src/AVEVA.Historian.Client/Grpc/HistorianGrpcTagClient.cs @@ -39,6 +39,27 @@ internal static class HistorianGrpcTagClient private static HistorianTagMetadata? GetTagMetadata(HistorianClientOptions options, string tag, CancellationToken cancellationToken) { byte[] tagInfos = GetTagInfosRaw(options, [tag], cancellationToken); + return ParseTagMetadata(tagInfos); + } + + // Spike/Phase-1 seam (pending.md A1): resolve tag metadata against an EXTERNALLY-supplied, + // already-authenticated connection + session — i.e. NO Create()/handshake here. The per-call + // GetTagMetadata and this seam share the parse tail (ParseTagMetadata) so neither duplicates the + // decode logic (DRY). + internal static HistorianTagMetadata? GetTagMetadataOnSession( + HistorianGrpcConnection connection, + HistorianGrpcHandshake.Session session, + string tag, + HistorianClientOptions options, + CancellationToken cancellationToken) + { + byte[] tagInfos = GetTagInfosRawOnSession(connection, session, [tag], options, cancellationToken); + return ParseTagMetadata(tagInfos); + } + + // Shared parse tail for both the per-call GetTagMetadata and the reuse-path GetTagMetadataOnSession. + private static HistorianTagMetadata? ParseTagMetadata(byte[] tagInfos) + { if (tagInfos.Length < 4) { return null; @@ -69,7 +90,19 @@ internal static class HistorianGrpcTagClient { using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options); HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, options, cancellationToken); + return GetTagInfosRawOnSession(connection, session, tags, options, cancellationToken); + } + // Spike/Phase-1 seam (pending.md A1): issue GetTagInfosFromName against an EXTERNALLY-supplied, + // already-authenticated connection + session — i.e. NO Create()/handshake here. GetTagInfosRaw + // delegates to this so the per-call path and the reuse path share one query implementation (DRY). + internal static byte[] GetTagInfosRawOnSession( + HistorianGrpcConnection connection, + HistorianGrpcHandshake.Session session, + IReadOnlyList tags, + HistorianClientOptions options, + CancellationToken cancellationToken) + { var retrievalClient = new GrpcRetrieval.RetrievalService.RetrievalServiceClient(connection.Channel); byte[] requestBuffer = BuildTagNamesBuffer(tags); GrpcRetrieval.GetTagInfosFromNameResponse response = retrievalClient.GetTagInfosFromName( @@ -112,6 +145,8 @@ internal static class HistorianGrpcTagClient return Task.Run(() => GetTagExtendedProperties(options, tag, cancellationToken), cancellationToken); } + // No …OnSession seam: extended-properties browse stays per-call (not amortized through the session + // pool — out of A1-broadening scope). Add a seam here only if the pool ever needs to route it. /// /// Issues a single page-0 GetTagExtendedPropertiesFromName call and returns the raw native /// btTeps response buffer (empty when the server reports no rows / non-success). Internal so @@ -222,6 +257,20 @@ internal static class HistorianGrpcTagClient { using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options); HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, options, cancellationToken); + return BrowseTagNamesOnSession(connection, session, filter, options, cancellationToken); + } + + // Spike/Phase-1 seam (pending.md A1): drive StartTagQuery → paged QueryTag → EndTagQuery against an + // EXTERNALLY-supplied, already-authenticated connection + session — i.e. NO Create()/handshake here. + // BrowseTagNames delegates to this so the per-call path and the reuse path share one browse + // implementation (DRY). + internal static List BrowseTagNamesOnSession( + HistorianGrpcConnection connection, + HistorianGrpcHandshake.Session session, + string filter, + HistorianClientOptions options, + CancellationToken cancellationToken) + { var retrievalClient = new GrpcRetrieval.RetrievalService.RetrievalServiceClient(connection.Channel); DateTime Deadline() => DateTime.UtcNow.Add(options.RequestTimeout); diff --git a/tests/AVEVA.Historian.Client.Tests/TagClientOnSessionSeamTests.cs b/tests/AVEVA.Historian.Client.Tests/TagClientOnSessionSeamTests.cs new file mode 100644 index 0000000..92a4078 --- /dev/null +++ b/tests/AVEVA.Historian.Client.Tests/TagClientOnSessionSeamTests.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using AVEVA.Historian.Client.Grpc; +using Xunit; + +namespace AVEVA.Historian.Client.Tests; + +public class TagClientOnSessionSeamTests +{ + [Theory] + [InlineData("BrowseTagNamesOnSession")] + [InlineData("GetTagInfosRawOnSession")] + [InlineData("GetTagMetadataOnSession")] + public void TagClient_ExposesOnSessionSeam(string name) + { + MethodInfo? m = typeof(HistorianGrpcTagClient).GetMethod( + name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public); + Assert.NotNull(m); + ParameterInfo[] ps = m!.GetParameters(); + Assert.Equal("HistorianGrpcConnection", ps[0].ParameterType.Name); + Assert.Equal("Session", ps[1].ParameterType.Name); + } +}