Event-row parser: verify against the provided 2023 R2 client; fix latent multi-row bug
Used the provided stock client as an oracle to verify the event read path. The capture-event harness returns 50 real events, and the instrument-grpc-nonstream rewrite captured the exact GetNextEventQueryResultBuffer.result buffer (63,192 bytes, version 0x0B=11, rowCount 50 = 25 Alarm.Set + 25 Alarm.Clear). Feeding that real buffer through HistorianEventRowProtocol.Parse exposed a latent parser bug. The real buffer layout is: version(2) + rowCount(4) + headerField(4, =0x1E) followed by MARKERLESS rows (rowFormat(2)=7 + filetime(8) + 8x u16 slots + compact-ascii type + propCount + props). The parser wrongly treated the one-time 0x1E field as a per-row marker and re-consumed [marker+format] for every row, so it decoded only the FIRST row of any multi-row buffer and stopped. This is not gRPC-specific: the captured WCF v9 buffer has the identical 0900 <rowCount> 1E000000 0700 header, so the shipped WCF event read had the same latent multi-row truncation. Fix: read a 10-byte buffer header (skip the 0x1E field once) and parse markerless rows; accept container version 9 (WCF) and 11 (gRPC), mirroring the interface-version gate that accepts History 11 and 12. Verified: the real 50-row buffer now decodes to exactly 50 events, ending cleanly at end-of-buffer (Parse_RealStockClientCapture_DecodesAllEvents, gated on HISTORIAN_EVENT_CAPTURE_NDJSON so it skips without the gitignored capture), plus a synthetic v11 golden test. 328 offline tests pass. The parse path is now verified against the provided client's real event data on both transports; the only remaining gap for gRPC events is the server delivering rows to our connection (the documented retrieval-server-gate). 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:
@@ -10,10 +10,10 @@ namespace AVEVA.Historian.Client.Wcf;
|
||||
/// native event read (instrument-wcf-readmessage record 24, two rows for Alarm.Set + Alarm.Clear):
|
||||
///
|
||||
/// <code>
|
||||
/// UInt16 version = 9
|
||||
/// UInt16 version = 9 (WCF) | 11 (2023 R2 gRPC)
|
||||
/// UInt32 rowCount
|
||||
/// UInt32 headerField = 0x1E // ONE buffer-level field (NOT a per-row marker)
|
||||
/// rowCount × Row {
|
||||
/// UInt32 rowMarker = 0x1E
|
||||
/// UInt16 rowFormat = 7
|
||||
/// Int64 eventTimeUtcFiletime
|
||||
/// UInt16 × 8 // purpose unclear (slot offsets?)
|
||||
@@ -42,10 +42,23 @@ namespace AVEVA.Historian.Client.Wcf;
|
||||
internal static class HistorianEventRowProtocol
|
||||
{
|
||||
public const ushort EventRowProtocolVersion = 9;
|
||||
public const uint RowMarker = 0x0000001Eu;
|
||||
public const ushort RowFormatV9 = 7;
|
||||
private const int HeaderSize = 6;
|
||||
private const int RowFixedHeaderSize = 4 + 2 + 8 + 16;
|
||||
|
||||
/// <summary>
|
||||
/// 2023 R2 gRPC returns the event-row buffer with container version <c>11</c> instead of the
|
||||
/// 2020 WCF <c>9</c>. The row layout is otherwise <b>byte-identical</b> (verified against a captured
|
||||
/// stock-client read: header <c>0B00 <rowCount> 1E000000</c> then markerless rows, 50
|
||||
/// Alarm.Set/Alarm.Clear rows decoded clean to end-of-buffer; the WCF v9 capture has the same
|
||||
/// <c>0900 <rowCount> 1E000000</c> header). Accept both, exactly as the interface-version gate
|
||||
/// accepts History 11 and 12.
|
||||
/// </summary>
|
||||
public const ushort EventRowProtocolVersionGrpc = 11;
|
||||
|
||||
/// <summary>Constant buffer-level field following <c>rowCount</c> (observed <c>0x1E</c>). NOT a
|
||||
/// per-row marker — it appears exactly once, before the first row.</summary>
|
||||
public const uint BufferHeaderField = 0x0000001Eu;
|
||||
public const ushort RowFormat = 7;
|
||||
private const int BufferHeaderSize = 2 + 4 + 4; // version + rowCount + headerField
|
||||
private const int RowFixedHeaderSize = 2 + 8 + 16; // rowFormat + filetime + 8×UInt16 slots
|
||||
|
||||
private const byte ValueTypeBool = 0x02;
|
||||
private const byte ValueTypeGuid = 0x10;
|
||||
@@ -55,25 +68,26 @@ internal static class HistorianEventRowProtocol
|
||||
|
||||
public static IReadOnlyList<HistorianEvent> Parse(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
if (buffer.Length < HeaderSize)
|
||||
if (buffer.Length < 6)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
ushort version = BinaryPrimitives.ReadUInt16LittleEndian(buffer[..2]);
|
||||
if (version != EventRowProtocolVersion)
|
||||
if (version != EventRowProtocolVersion && version != EventRowProtocolVersionGrpc)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
uint rowCount = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(2, 4));
|
||||
if (rowCount == 0)
|
||||
if (rowCount == 0 || buffer.Length < BufferHeaderSize)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
List<HistorianEvent> events = new(checked((int)rowCount));
|
||||
int cursor = HeaderSize;
|
||||
// Skip the single buffer-level header field (0x1E); rows follow with NO per-row marker.
|
||||
int cursor = BufferHeaderSize;
|
||||
for (uint rowIndex = 0; rowIndex < rowCount; rowIndex++)
|
||||
{
|
||||
if (!TryReadRow(buffer, ref cursor, out HistorianEvent? row))
|
||||
@@ -95,19 +109,13 @@ internal static class HistorianEventRowProtocol
|
||||
return false;
|
||||
}
|
||||
|
||||
uint marker = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(cursor, 4));
|
||||
if (marker != RowMarker)
|
||||
ushort format = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(cursor, 2));
|
||||
if (format != RowFormat)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ushort format = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(cursor + 4, 2));
|
||||
if (format != RowFormatV9)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
long filetime = BinaryPrimitives.ReadInt64LittleEndian(buffer.Slice(cursor + 6, 8));
|
||||
long filetime = BinaryPrimitives.ReadInt64LittleEndian(buffer.Slice(cursor + 2, 8));
|
||||
DateTime eventTimeUtc = DateTime.FromFileTimeUtc(filetime);
|
||||
int afterFixedHeader = cursor + RowFixedHeaderSize;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user