From ecc8c97a802e2eeb031664f121db194576896de2 Mon Sep 17 00:00:00 2001 From: Loretta Date: Thu, 11 Jun 2026 08:52:09 +0200 Subject: [PATCH] =?UTF-8?q?EK=C3=81ER=20grid:=20status=20enum=20fix,=20bat?= =?UTF-8?q?ch=20create,=20UI/UX,=20DB=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed EkaerHistory status persistence (int+enum wrapper for linq2db) - Added batch EKÁER record creation (toolbar date picker, SignalR, UI) - Enhanced GridEkaerHistory: status, error, XML copy, manual edit - Improved column display format handling in MgGridInfoPanel - Updated SignalR binary protocol streaming strategy doc - Added DB guard PowerShell hook: blocks non-_DEV DB shell access - New styles for EKÁER toolbar date picker --- .../SIGNALR_BINARY_PROTOCOL_ISSUES.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/SIGNALR_BINARY_PROTOCOL_ISSUES.md b/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/SIGNALR_BINARY_PROTOCOL_ISSUES.md index c493229..87a4e93 100644 --- a/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/SIGNALR_BINARY_PROTOCOL_ISSUES.md +++ b/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/SIGNALR_BINARY_PROTOCOL_ISSUES.md @@ -27,12 +27,14 @@ Resolved — `TryParseChunkData` runs the blocking streaming deser off the .NET **Why it's harder than the deser side:** the blocking sits inside `IHubProtocol.WriteMessage` (synchronous, called by SignalR on a pool thread, must return when the send is done). Unlike the receive side — where *we* spawned the deser task and could move it off-pool — we don't own the calling thread here, so the "off-pool dedicated thread" trick does not apply. Within the sync `WriteMessage` contract the choice is binary: flush per chunk (bounded memory, **blocks**) or buffer the whole message + one async flush (no block, **multi-MB memory** = effectively Bytes). "Bounded memory + no block" is not reachable inside `WriteMessage`. **Possible solutions:** -- **A) SignalR native streaming** (`IAsyncEnumerable` / `ChannelReader`) — async backpressure, no block, bounded memory. Architecturally correct, but the consumer's dispatch is **tag-based** (`OnReceiveMessage(tag, requestId, SignalParams, data)`), not SignalR's invocation/streaming model → needs rework to fit. -- **B) Application-level batching** — send a large collection as N small Bytes-mode messages (batch index / is-last in `SignalParams`), client reassembles. Fits the tag protocol; small messages avoid the blocking flush; bounded memory + good latency. May reuse the `AcSignalRDataSource` merge pattern. *(Pragmatic favourite given the tag-based dispatch.)* -- **C) Mitigation only** — `FlushPolicy.Coalesced` + lower `FlushTimeout` reduce blocking frequency/duration but do not eliminate it under sustained backpressure. Band-aid. -- **D) Stay on Bytes** — whole-message byte[] + single async flush. No block; multi-MB memory per concurrent send. **Currently in production and stable** — so the hang is already gone; streaming would be an optimization (latency-to-first-byte / bounded memory), not a necessity. - -Direction depends on the actual goal (bounded memory → B; latency-to-first-byte → A or B; Bytes already sufficient → D). To be decided. +- **A) Per-item SignalR native streaming** (existing `StreamAllAsync` ↔ `OnReceiveStreamMessage` path) — async backpressure, no block. **Rejected: loses whole-graph dedup** — code-verified: `EnumerateGenericAsync` serializes PER ITEM (`SignalRSerializationHelper.SerializeToBinary` per element = independent serialization context each), so cross-item idtracking + string-interning is lost. +- **B) Application-level batching** — N small Bytes messages, client reassembles. Same dedup loss across batches. Rejected for the same reason. +- **C) Mitigation only** — `FlushPolicy.Coalesced` + lower `FlushTimeout` reduce blocking frequency but do not eliminate it under sustained backpressure. Band-aid. +- **D) Stay on Bytes** — no block, but multi-MB byte[] (LOH) per send, no streaming. Current production hotfix; not the target state. +- **E) Whole-graph chunk-stream over SignalR native streaming — leading candidate (code-verified design; prototype pending).** + - **Server**: a dedicated off-pool thread (mirror of `AcBinaryDeserExecutor`) runs the **existing** `AcBinarySerializer.SerializeChunked(value, pipe, options)` (Pipe overload, `AcBinarySerializer.cs:527`; **raw mode** — avoids the framed-mode 65 KB single-value cap `ACCORE-BIN-I-B7K9`) into an internal `Pipe`. This is ONE serialize pass (single `ScanForDuplicates` + one context) → **whole-graph idtracking + string-interning preserved**. Its backpressure block (`SyncAwaitFlush` → `Task.Wait`, `AsyncPipeWriterOutput.cs:207-217`) lands on the dedicated thread, NOT the pool. `OnReceiveStreamMessage` async-reads `pipe.Reader` and yields `byte[]` chunks → SignalR native streaming carries them with async backpressure. The byte[] StreamItems bypass the chunked `WriteMessage`/`SyncFlush` path entirely (`HasStreamableArgs` returns false for byte[], `AcBinaryHubProtocol.cs:408`). + - **Client**: NEW consumer (today `StreamAllAsync` deserializes each chunk as a complete message — reassembly does not exist yet): `await foreach` feeds chunks into one `AsyncPipeReaderInput(multiMessage: false)` (raw verbatim mode, `AsyncPipeReaderInput.cs:181,218-223`), the whole-graph deserialize runs off-pool via `AcBinaryDeserExecutor` (same assembly), join only AFTER `Complete()`. WASM: accumulate + synchronous deserialize on completion (mirror of the protocol's `IsBrowser` fallback). + - **Open points (prototype/test before commit)**: client composition deadlock discipline (deser MUST be off-pool; the feed loop must never block on the deser task before `Complete()` — same bug class as the deser-side fix); WASM fallback wiring; `UseCompression` unsupported on the PipeWriter path; perf of the extra Pipe hop + dedicated send thread (benchmark vs Bytes). ### Related TODO