feat(mtconnect): current/sample parse + sequence-gap detection (Task 7)
Adds the /current and /sample legs to MTConnectAgentClient: - MTConnectStreamsParser: hand-rolled System.Xml.Linq parse of an MTConnectStreams document into MTConnectStreamsResult. Observations are every element child of <Samples>/<Events>/<Condition>, keyed by dataItemId (the element NAME is the data item's type in PascalCase, so a fixed name set would drop observations). A CONDITION's value is its element name, not its text - <Normal/> is empty - and <Unavailable/> normalizes onto the same UNAVAILABLE sentinel a Sample/Event carries as text, so Task 8's no-comms mapping sees one token. Timestamps are normalized to DateTimeKind.Utc explicitly. - IsSequenceGap(requestedFrom, chunk) => chunk.FirstSequence > requestedFrom, so Task 11's pump can re-baseline after a ring-buffer overflow. Equality is NOT a gap. - SampleAsync: ResponseHeadersRead + a hand-rolled multipart/x-mixed-replace frame reader (Content-length fast path, boundary-scan fallback), bounded by a heartbeat watchdog derived from HeartbeatMs so a silent Agent faults instead of wedging the pump - the S7 frozen-peer shape (arch-review R2-01). The long poll gets its own HttpClient with an infinite Timeout so the unary /probe + /current deadline stays bounded. Task 0 package decision, option (c): both TrakHound PackageReferences and their PackageVersions are dropped. MTConnectHttpClientStream takes a URL and dials its own socket (no handler/stream seam, so it cannot serve a socket-free unit suite), and it emits already-parsed documents through the formatter lookup Task 6 proved is missing - framing-only reuse was never actually on offer. The driver now depends on nothing but the BCL. Recorded in the plan's Task 0 CORRECTION block. Also folds in two Task 6 review carry-overs: the stale IMTConnectAgentClient doc comment claiming a TrakHound-backed implementation, and a probe test constructing a mixed-namespace vendor extension (<x:Pump> with default-namespace <DataItems> children) that backs the parser's ignore-the-namespace claim with a test. 187/187 green.
This commit is contained in:
@@ -96,8 +96,53 @@ owns the final call:** (a) use `-HTTP` for framing only, (b) add `MTConnect.NET-
|
||||
wholesale, or (c) hand-roll the boundary reader and **drop both package references** — in which case the
|
||||
Task-0 rationale comment in `Directory.Packages.props` goes with them. Record the outcome here.
|
||||
|
||||
**OUTCOME (Task 7) — option (c): hand-rolled boundary reader, BOTH package references DROPPED.**
|
||||
`MTConnect.NET-Common`, `MTConnect.NET-HTTP` and the transitive `MTConnect.NET-TLS` are gone from
|
||||
`Directory.Packages.props` and from `Driver.MTConnect.csproj`; the Task-0 rationale comments went with
|
||||
them (each replaced by a comment naming this block). **The driver now depends on nothing but the BCL** —
|
||||
`HttpClient` + `System.Xml.Linq`. Reflection over `MTConnect.NET-HTTP` 6.9.0.2 (net9.0 lib, with
|
||||
`-Common` resolved alongside) settled the one open candidate:
|
||||
|
||||
```
|
||||
TYPE MTConnect.Clients.MTConnectHttpClientStream
|
||||
.ctor(String url, String documentFormat)
|
||||
Void Start(CancellationToken), Void Stop(), Task Run(CancellationToken)
|
||||
event DocumentReceived / ErrorReceived / FormatError / ConnectionError / …
|
||||
```
|
||||
|
||||
Three independent disqualifiers, any one of which is fatal:
|
||||
|
||||
1. **It cannot be exercised without a socket.** Its only constructor takes a *URL* and it dials the
|
||||
connection itself inside `Run` — there is no `HttpMessageHandler`, no `HttpClient`, and no `Stream`
|
||||
injection point. The whole `IMTConnectAgentClient` seam exists so Tasks 6–13 are unit-testable
|
||||
against canned XML with no listener; adopting this type would have forced a live agent (or a real
|
||||
loopback HTTP server) into the unit suite.
|
||||
2. **"Framing alone" was never actually on offer.** The type does not surface raw part payloads — it
|
||||
raises `DocumentReceived` with an already-parsed `IDocument`, resolved through the same
|
||||
`documentFormat` formatter lookup that Task 6 proved is missing. Using it for framing would still
|
||||
have required adding `MTConnect.NET-XML`, i.e. option (b) in disguise.
|
||||
3. **The shape is inverted and the payload is heavy.** An event-driven `Start`/`Run` object has to be
|
||||
adapted back into the `IAsyncEnumerable<MTConnectStreamsResult>` the seam declares, and `-HTTP`
|
||||
vendors an entire embedded web server (`Ceen.*` types) that this driver would never touch.
|
||||
|
||||
The replacement is `MultipartStreamReader`, a ~200-line private nested class in
|
||||
`MTConnectAgentClient.cs` that frames `multipart/x-mixed-replace` off the response stream. Two paths:
|
||||
`Content-length` present (every agent in the wild writes it) → read by length and emit the chunk the
|
||||
instant it completes; absent → scan to the next boundary, which necessarily emits one chunk late and
|
||||
exists as a correctness fallback only. Both paths are covered by tests. Every read is bounded by a
|
||||
heartbeat watchdog (`2 × HeartbeatMs + RequestTimeoutMs`) so a silent agent faults instead of wedging
|
||||
the pump — the R2-01 shape — and the buffer is capped at 32 MiB.
|
||||
|
||||
**Also settled here: the streaming leg gets its own `HttpClient`.** `HttpClient.Timeout` is a
|
||||
per-instance whole-response deadline, so the long poll cannot share the unary client. Raising the one
|
||||
timeout would have un-bounded `/probe` + `/current`, which R2-01 forbids; instead `_streamHttp` runs
|
||||
`Timeout.InfiniteTimeSpan` and is bounded by the request deadline on its *header* phase plus the
|
||||
watchdog on its body.
|
||||
|
||||
**Process lesson:** "the dependency restores" is not "the dependency does the job." A library
|
||||
verification checklist needs one end-to-end call against a real input, not just a restore.
|
||||
verification checklist needs one end-to-end call against a real input, not just a restore. Task 7 adds
|
||||
a second: **check the constructor for a test seam before counting a library as usable** — a type that
|
||||
can only be handed a URL cannot participate in a socket-free unit suite, however good its internals.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user