Relocate AcSignalRDataSource to AyCode.Services

Moved AcSignalRDataSource and TrackingItemHelpers from AyCode.Services.Server to AyCode.Services (namespace updated). Updated all references, usings, and doc cross-refs to new location. Removed orphan SignalR.Core 1.2.0 package and .Server DLL refs from Blazor/Web.Client/Shared projects; WASM AOT build now works. Introduced AcBinaryDeserExecutor for off-pool streaming deserialization, fixing thread pool starvation (see updated protocol docs). Updated test usings and doc tables. No functional changes to DataSource logic. Decision log entry LLMP-DEC-68 documents the change.
This commit is contained in:
Loretta 2026-06-08 10:43:10 +02:00
parent aed36a71cb
commit bb1dbd242e
26 changed files with 209 additions and 55 deletions

View File

@ -139,7 +139,15 @@
"Bash(sed -n '50108,58000p' jitasm_tier1.txt)", "Bash(sed -n '50108,58000p' jitasm_tier1.txt)",
"Bash(awk '/^; Assembly listing for method AyCode.Core.Tests.TestModels.TestOrder_All_False_GeneratedWriter:WriteProperties/{found=1; next} found && /^; Assembly listing for method/{exit} found && /mov[[:space:]]+byte[[:space:]]+ptr/{print}' jitasm_tier1.txt)", "Bash(awk '/^; Assembly listing for method AyCode.Core.Tests.TestModels.TestOrder_All_False_GeneratedWriter:WriteProperties/{found=1; next} found && /^; Assembly listing for method/{exit} found && /mov[[:space:]]+byte[[:space:]]+ptr/{print}' jitasm_tier1.txt)",
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" log --oneline -15 -- \"AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs\")", "Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" log --oneline -15 -- \"AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs\")",
"Bash(dotnet workload *)" "Bash(dotnet workload *)",
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" mv \"AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs\" \"AyCode.Services/SignalRs/AcSignalRDataSource.cs\")",
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" mv \"AyCode.Services.Server/SignalRs/TrackingItemHelpers.cs\" \"AyCode.Services/SignalRs/TrackingItemHelpers.cs\")",
"Bash(echo \"=== EXIT $? ===\")",
"Bash(rm -rf \"H:/Applications/Mango/Source/FruitBankHybridApp/FruitBankHybrid.Web.Client/obj\" \"H:/Applications/Aycode/Source/AyCode.Blazor/AyCode.Blazor.Components/obj\" && dotnet publish \"H:/Applications/Mango/Source/FruitBankHybridApp/FruitBankHybrid.Web.Client/FruitBankHybrid.Web.Client.csproj\" -c Release 2>&1)",
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" mv \"AyCode.Services.Server/docs/SIGNALR_DATASOURCE\" \"AyCode.Services/docs/SIGNALR_DATASOURCE\")",
"Bash(awk 'NR==135' \"H:/Applications/Aycode/Source/AyCode.Core/.github/LLM_PROTOCOL_DECISIONS.md\")",
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" status --short)",
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" reset)"
] ]
} }
} }

File diff suppressed because one or more lines are too long

View File

@ -148,7 +148,7 @@ Full doctrine: `../docs/ARCHITECTURE.md#framework-vs-consumer-boundary`
## SignalR ## SignalR
7. **Tag-based transport (no conventional hub methods)** — SignalR communication should generally use the generic methods provided by `AcWebSignalRHubBase` (server) and `AcSignalRClientBase` (client). Request types are conventionally identified by `int` tags. Try to avoid adding custom, business-specific, or conventional string-based Hub methods (e.g., `GetUsers()`). 7. **Tag-based transport (no conventional hub methods)** — SignalR communication should generally use the generic methods provided by `AcWebSignalRHubBase` (server) and `AcSignalRClientBase` (client). Request types are conventionally identified by `int` tags. Try to avoid adding custom, business-specific, or conventional string-based Hub methods (e.g., `GetUsers()`).
8. **AcSignalRDataSource** — generic `IList<T>` with change tracking, CRUD via `SignalRCrudTags`, binary merge, rollback. See `AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md`. Transport docs: `AyCode.Services/docs/SIGNALR/README.md`. 8. **AcSignalRDataSource** — generic `IList<T>` with change tracking, CRUD via `SignalRCrudTags`, binary merge, rollback. See `AyCode.Services/docs/SIGNALR_DATASOURCE/README.md`. Transport docs: `AyCode.Services/docs/SIGNALR/README.md`.
9. ~~**JSON-in-Binary tech debt**~~**REMOVED** 2026-04-26 (resolved: `AyCode.Core/docs/XCUT/XCUT_ISSUES.md#accore-xcut-i-x8q1`). Number kept reserved to prevent renumbering of subsequent items. 9. ~~**JSON-in-Binary tech debt**~~**REMOVED** 2026-04-26 (resolved: `AyCode.Core/docs/XCUT/XCUT_ISSUES.md#accore-xcut-i-x8q1`). Number kept reserved to prevent renumbering of subsequent items.
## Critical Warnings ## Critical Warnings

View File

@ -16,7 +16,7 @@ To make IDs like `ACCORE-LOG-I-K7M2`, `ACCORE-SIG-B-3R9P`, `ACCORE-XCUT-I-A4B7`
| `AUTH` | AUTH | User authentication: bearer tokens, JWT, login flow, hub authorization | `AyCode.Core/docs/AUTH/` | | `AUTH` | AUTH | User authentication: bearer tokens, JWT, login flow, hub authorization | `AyCode.Core/docs/AUTH/` |
| `SIG` | SIGNALR | SignalR transport: tags, client base, dispatch, session | `AyCode.Core/AyCode.Services/docs/SIGNALR/` (+ variant in `AyCode.Services.Server`) | | `SIG` | SIGNALR | SignalR transport: tags, client base, dispatch, session | `AyCode.Core/AyCode.Services/docs/SIGNALR/` (+ variant in `AyCode.Services.Server`) |
| `SBP` | SIGNALR_BINARY_PROTOCOL | Binary wire protocol over SignalR: framing, chunking, argument read | `AyCode.Core/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/` | | `SBP` | SIGNALR_BINARY_PROTOCOL | Binary wire protocol over SignalR: framing, chunking, argument read | `AyCode.Core/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/` |
| `SIGDS` | SIGNALR_DATASOURCE | Client-server DataSource on SignalR transport: change tracking, rollback, sync state, load lifecycle, IList<T> wrapper | `AyCode.Core/AyCode.Services.Server/docs/SIGNALR_DATASOURCE/` | | `SIGDS` | SIGNALR_DATASOURCE | Client-server DataSource on SignalR transport: change tracking, rollback, sync state, load lifecycle, IList<T> wrapper | `AyCode.Core/AyCode.Services/docs/SIGNALR_DATASOURCE/` |
| `BIN` | BINARY | AcBinary serializer: features, format, writers, source generator | `AyCode.Core/AyCode.Core/docs/BINARY/` | | `BIN` | BINARY | AcBinary serializer: features, format, writers, source generator | `AyCode.Core/AyCode.Core/docs/BINARY/` |
| `TOON` | TOON | Toon serializer: LLM-optimized format with @meta/@types/@data sections | `AyCode.Core/AyCode.Core/docs/TOON/` | | `TOON` | TOON | Toon serializer: LLM-optimized format with @meta/@types/@data sections | `AyCode.Core/AyCode.Core/docs/TOON/` |
| `XCUT` | cross-cutting | Issues / TODOs spanning ≥2 ACCORE topics — one canonical home, referenced from each affected topic | `AyCode.Core/AyCode.Core/docs/XCUT/` | | `XCUT` | cross-cutting | Issues / TODOs spanning ≥2 ACCORE topics — one canonical home, referenced from each affected topic | `AyCode.Core/AyCode.Core/docs/XCUT/` |

View File

@ -209,7 +209,7 @@ The framework explicitly does NOT provide type-dispatch infrastructure (no wire-
This is a permanent architectural decision, not a "TODO not yet done." Do not file new TODOs proposing wire-format type-id, registry, or session/handshake infrastructure inside `AyCode.Core`. This is a permanent architectural decision, not a "TODO not yet done." Do not file new TODOs proposing wire-format type-id, registry, or session/handshake infrastructure inside `AyCode.Core`.
**Workarounds (canonical patterns the consumer implements):** **Workarounds (canonical patterns the consumer implements):**
- **Tag-based dispatch above AcBinary** (recommended — matches CLAUDE.md Rule #7): prefix each message with an `int` (or enum) tag the consumer reads first to choose `Deserialize<TypeForTag>`. The consumer encapsulates the tag-read + type-dispatch in its own deserialization wrapper. Reference example: `AyCode.Services.Server/SignalRs/AcSignalRDataSource` uses `SignalRCrudTags`. - **Tag-based dispatch above AcBinary** (recommended — matches CLAUDE.md Rule #7): prefix each message with an `int` (or enum) tag the consumer reads first to choose `Deserialize<TypeForTag>`. The consumer encapsulates the tag-read + type-dispatch in its own deserialization wrapper. Reference example: `AyCode.Services/SignalRs/AcSignalRDataSource` uses `SignalRCrudTags`.
- **Polymorphic envelope type**: define a single `T = Envelope` containing a discriminator field + raw payload bytes; the consumer deserializes the envelope, switches on the discriminator, and re-deserializes the payload as the concrete type from the inner `byte[]`. Adds a small layer of indirection but works on top of fix-T `Deserialize<Envelope>`. - **Polymorphic envelope type**: define a single `T = Envelope` containing a discriminator field + raw payload bytes; the consumer deserializes the envelope, switches on the discriminator, and re-deserializes the payload as the concrete type from the inner `byte[]`. Adds a small layer of indirection but works on top of fix-T `Deserialize<Envelope>`.
- **One input per type-stream**: separate streams per message-class. Practical when the type-set is small and the transport can afford multiple connections. - **One input per type-stream**: separate streams per message-class. Practical when the type-set is small and the transport can afford multiple connections.

View File

@ -1,6 +1,7 @@
using AyCode.Core.Enums; using AyCode.Core.Enums;
using AyCode.Core.Tests.TestModels; using AyCode.Core.Tests.TestModels;
using AyCode.Services.Server.SignalRs; using AyCode.Services.Server.SignalRs;
using AyCode.Services.SignalRs;
namespace AyCode.Services.Server.Tests.SignalRs.SignalRDatasources; namespace AyCode.Services.Server.Tests.SignalRs.SignalRDatasources;

View File

@ -11,7 +11,6 @@ Server-side service implementations: JWT authentication, SendGrid email delivery
| Document | Topic | | Document | Topic |
|---|---| |---|---|
| `SIGNALR/README.md` | Server-side SignalR hub (dispatch, session, broadcast) | | `SIGNALR/README.md` | Server-side SignalR hub (dispatch, session, broadcast) |
| `SIGNALR_DATASOURCE/README.md` | Real-time DataSource with CRUD & change tracking |
## Folder Structure ## Folder Structure
@ -19,7 +18,7 @@ Server-side service implementations: JWT authentication, SendGrid email delivery
|---|---| |---|---|
| [`Emails/`](Emails/README.md) | SendGrid email service (registration, password reset) | | [`Emails/`](Emails/README.md) | SendGrid email service (registration, password reset) |
| [`Logins/`](Logins/README.md) | Server-side login with JWT token generation | | [`Logins/`](Logins/README.md) | Server-side login with JWT token generation |
| [`SignalRs/`](SignalRs/README.md) | SignalR hub base, session service, data source with change tracking, client broadcast | | [`SignalRs/`](SignalRs/README.md) | SignalR hub base, session service, client broadcast |
## Dependencies ## Dependencies

View File

@ -1,6 +1,6 @@
# SignalRs # SignalRs
Server-side SignalR hub infrastructure: hub base class, session management, data source with change tracking, and client broadcast service. Server-side SignalR hub infrastructure: hub base class, session management, and client broadcast service.
> **Architecture:** For full dispatch flow, tag system, and tech debt documentation see `AyCode.Services/docs/SIGNALR/README.md`. > **Architecture:** For full dispatch flow, tag system, and tech debt documentation see `AyCode.Services/docs/SIGNALR/README.md`.
@ -18,17 +18,9 @@ Server-side SignalR hub infrastructure: hub base class, session management, data
### Data Source ### Data Source
> **Full specification:** `AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md` > **Relocated (LLMP-DEC-68):** `AcSignalRDataSource` — the client-side real-time collection — moved to `AyCode.Services/SignalRs/` and is **no longer part of this server-side assembly**. Full spec: `AyCode.Services/docs/SIGNALR_DATASOURCE/README.md`.
- **`AcSignalRDataSource.cs`** — Generic real-time collection (`AcSignalRDataSource<TDataItem, TId, TIList>`) implementing `IList<T>` with full CRUD and change tracking.
- **Change tracking:** `TrackingItem<T, TId>` wraps each modified item with `TrackingState` + `OriginalValue` for rollback. `ChangeTracking<T, TId>` manages the tracking list.
- **Loading:** `LoadDataSource()` (sync), `LoadDataSourceAsync()` (async callback), `LoadItem(id)` (single). Binary path uses `BinaryToMerge()` for `AcObservableCollection` (batch UI update via `BeginUpdate/EndUpdate`).
- **Saving:** `SaveChanges()` iterates tracked items, posts each via CRUD tag, rollbacks on failure. `SaveItem()` for individual saves.
- **Sync state:** `IsSyncing` (Interlocked counter) + `OnSyncingStateChanged` event for UI loading indicators.
- **Locking:** `object _syncRoot` (sync ops) + `SemaphoreSlim _asyncLock` (async ops). `GetEnumerator()` returns safe copy.
- **Working reference list:** `SetWorkingReferenceList()` allows external list to become inner storage (zero-copy).
- **Context:** `ContextIds` (object[]) + `FilterText` (string) sent with GetAll requests for server-side filtering.
### Utilities ### Utilities
- **`ExtensionMethods.cs`** — `InvokeMethod()` — invokes methods and unwraps `Task`/`Task<T>`/`ValueTask` results. - **`ExtensionMethods.cs`** — `InvokeMethod()` — invokes methods and unwraps `Task`/`Task<T>`/`ValueTask` results.
- **`TrackingItemHelpers.cs`** — Deep clone helpers: `JsonClone<T>()`, `ReflectionClone<T>()`.
> `TrackingItemHelpers.cs` (deep-clone helpers used by the data source) relocated to `AyCode.Services/SignalRs/` alongside `AcSignalRDataSource` — see LLMP-DEC-68.

View File

@ -5,7 +5,8 @@ Topic docs for the `AyCode.Services.Server` project (Layer 0, server-side servic
## Topics ## Topics
- [`SIGNALR/`](SIGNALR/README.md) — Server-side SignalR hub (dispatch, session, broadcast) - [`SIGNALR/`](SIGNALR/README.md) — Server-side SignalR hub (dispatch, session, broadcast)
- [`SIGNALR_DATASOURCE/`](SIGNALR_DATASOURCE/README.md) — Client-server DataSource on SignalR transport (change tracking, rollback, sync state, IList<T> wrapper)
> **Note:** `SIGNALR_DATASOURCE/` (the `AcSignalRDataSource` client-side collection) relocated to `AyCode.Services/docs/SIGNALR_DATASOURCE/` — see `LLMP-DEC-68`.
## Navigation ## Navigation

View File

@ -3,7 +3,7 @@
Server-side SignalR hub infrastructure: method dispatch, session management, broadcast, and diagnostics. Source: `SignalRs/` in this project. Server-side SignalR hub infrastructure: method dispatch, session management, broadcast, and diagnostics. Source: `SignalRs/` in this project.
> For client-side transport (tags, wire protocol, client base) see `AyCode.Services/docs/SIGNALR/README.md`. > For client-side transport (tags, wire protocol, client base) see `AyCode.Services/docs/SIGNALR/README.md`.
> For the DataSource collection see `../SIGNALR_DATASOURCE/README.md`. > For the DataSource collection see `AyCode.Services/docs/SIGNALR_DATASOURCE/README.md` (relocated to the client-side project).
## Server Processing ## Server Processing

View File

@ -0,0 +1,104 @@
using System.Collections.Concurrent;
namespace AyCode.Services.SignalRs;
/// <summary>
/// Dedicated off-pool executor for the AsyncSegment streaming deserialization (see
/// <c>SIGNALR_BINARY_PROTOCOL_ISSUES.md#accore-sbp-i-t3w9</c>).
///
/// <para>The streaming deser blocks in <c>AsyncPipeReaderInput.TryAdvanceSegment</c> →
/// <c>ManualResetEventSlim.Wait()</c> between network-delivered chunks. Running that blocking
/// wait on a .NET thread-pool thread (plain <c>Task.Run</c>) starves the pool: the signal that
/// unblocks it (the producer <c>Feed()</c>+<c>Set()</c>) runs on the SignalR receive-continuation,
/// which ALSO needs a pool thread → circular dependency → deadlock under concurrent load + real
/// network latency.</para>
///
/// <para><b>Design — never-queue, grow-on-demand, reuse-forever:</b> every <see cref="Run"/>
/// immediately hands the work to an idle worker (reuse) or spawns a new dedicated background
/// thread (grow). It <b>never queues</b>. Consequences:</para>
/// <list type="bullet">
/// <item>The deser always starts immediately on its own off-pool thread → the <c>[202]</c>
/// <c>GetResult()</c> always waits on a <i>running</i> deser (no queue-coupling).</item>
/// <item>The blocking <c>Wait()</c> never consumes a .NET thread-pool thread → the producer's
/// receive-continuations always get pool threads → no circular dependency, no starvation.</item>
/// <item>Threads are reused (no per-message churn). Thread count is bounded in practice by peak
/// concurrent chunked receives — one in-flight deser per connection (the <c>[202]</c>
/// <c>GetResult</c> drains it before the next message on that connection).</item>
/// </list>
///
/// <para>This is <c>TaskCreationOptions.LongRunning</c>'s unconditional safety (never-queue,
/// off-pool) plus thread reuse. Workers live forever — no idle-timeout, hence no idle-vs-assign
/// race (the simplest provably-correct variant). An idle-timeout reclaim can be added later if the
/// held-thread memory at peak concurrency becomes a concern; it must use a CAS idle→dead transition.</para>
///
/// <para><b>Rejected:</b> a bounded-queue executor reintroduces the <c>GetResult</c>-on-queued-deser
/// coupling and drops requests on overload.</para>
/// </summary>
internal static class AcBinaryDeserExecutor
{
private static readonly ConcurrentStack<Worker> _idle = new();
// Monotonic id for distinct worker-thread names — eases dump / profiler / debug correlation.
// Incremented only on grow (new thread), never per message.
private static int _threadId;
/// <summary>
/// Runs <paramref name="work"/> on a dedicated off-pool thread (reused idle worker, or a new
/// one) — never queues. Returns a task completing with the work's result.
/// </summary>
public static Task<object?> Run(Func<object?> work)
{
var tcs = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
if (_idle.TryPop(out var worker))
worker.Assign(work, tcs); // reuse an idle worker
else
new Worker().Start(work, tcs); // grow: spawn a new dedicated thread
return tcs.Task;
}
private sealed class Worker
{
// Single-permit park signal. Released exactly once per park (Assign), so the count
// never exceeds 1 → no SemaphoreFullException. Release/Wait provide the memory barrier
// that publishes the _work/_tcs writes to the worker thread.
private readonly SemaphoreSlim _signal = new(0, 1);
private Func<object?>? _work;
private TaskCompletionSource<object?>? _tcs;
public void Start(Func<object?> work, TaskCompletionSource<object?> tcs)
{
_work = work;
_tcs = tcs;
new Thread(Loop) { IsBackground = true, Name = $"AcBinaryDeser-{Interlocked.Increment(ref _threadId)}" }.Start();
}
// Called by Run only when this worker is parked in _signal.Wait() (it was popped from
// _idle, which it entered only AFTER finishing the previous work). So _work/_tcs are
// written here strictly while the worker is idle — no race with the worker's active read.
public void Assign(Func<object?> work, TaskCompletionSource<object?> tcs)
{
_work = work;
_tcs = tcs;
_signal.Release();
}
private void Loop()
{
while (true)
{
var work = _work ?? throw new InvalidOperationException("AcBinaryDeserExecutor worker woke without work — invariant violated");
var tcs = _tcs ?? throw new InvalidOperationException("AcBinaryDeserExecutor worker woke without tcs — invariant violated");
_work = null;
_tcs = null;
try { tcs.SetResult(work()); }
catch (Exception ex) { tcs.SetException(ex); }
_idle.Push(this); // become reusable
_signal.Wait(); // park until the next Assign
}
}
}
}

View File

@ -929,10 +929,20 @@ public class AcBinaryHubProtocol : IHubProtocol
// Lazy start: begin background deserialization after first chunk is written. // Lazy start: begin background deserialization after first chunk is written.
// The deser task reads via AsyncPipeReaderInputAdapter (struct over class) which // The deser task reads via AsyncPipeReaderInputAdapter (struct over class) which
// calls TryAdvanceSegment on the input — blocks on ManualResetEventSlim.Wait when // calls TryAdvanceSegment on the input — blocks on ManualResetEventSlim.Wait when
// out of data. Browser fallback: skip Task.Run — the MRES.Wait throws // out of data. Browser fallback: skip the background task — the MRES.Wait throws
// PlatformNotSupportedException on WASM. Instead, buffer all chunks and run the // PlatformNotSupportedException on WASM. Instead, buffer all chunks and run the
// deserializer synchronously on CHUNK_END, where state.Input.Complete() has // deserializer synchronously on CHUNK_END, where state.Input.Complete() has
// already been called → TryAdvanceSegment never enters the Wait path. // already been called → TryAdvanceSegment never enters the Wait path.
//
// CRITICAL — off-pool via AcBinaryDeserExecutor, NOT the .NET thread pool. The deser
// BLOCKS in ManualResetEventSlim.Wait() between network-delivered chunks. On the
// thread pool (plain Task.Run) this pins a pool thread for the message's whole
// network-transfer; the signal that unblocks it (the producer Feed()+Set()) runs on
// the SignalR receive-continuation, which ALSO needs a pool thread → circular
// dependency → pool starvation → "stuck loading → 60 s timeout" hang under concurrent
// load. AcBinaryDeserExecutor runs the blocking wait on a dedicated, reused, never-
// queued thread (see SIGNALR_BINARY_PROTOCOL_ISSUES.md#accore-sbp-i-t3w9) → pool stays
// free for producers, no coupling, streaming overlap preserved, no churn.
if (state.DeserTask == null && !IsBrowser) if (state.DeserTask == null && !IsBrowser)
{ {
_logger?.LogDebug("TryParseChunkData starting background deserialization targetType={TargetType}", state.StreamedArgType.Name); _logger?.LogDebug("TryParseChunkData starting background deserialization targetType={TargetType}", state.StreamedArgType.Name);
@ -941,7 +951,7 @@ public class AcBinaryHubProtocol : IHubProtocol
var type = state.StreamedArgType; var type = state.StreamedArgType;
var opts = _options; var opts = _options;
state.DeserTask = Task.Run(() => AcBinaryDeserializer.Deserialize(input2, type, opts)); state.DeserTask = AcBinaryDeserExecutor.Run(() => AcBinaryDeserializer.Deserialize(input2, type, opts));
} }
input = input.Slice(totalNeeded); input = input.Slice(totalNeeded);

View File

@ -7,13 +7,12 @@ using AyCode.Core.Loggers;
using AyCode.Core.Serializers; using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Binaries;
using AyCode.Services.SignalRs; using AyCode.Services.SignalRs;
using Castle.Core.Logging;
using System.Collections; using System.Collections;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
namespace AyCode.Services.Server.SignalRs namespace AyCode.Services.SignalRs
{ {
//public class TrackingItemGuid<TDataItem>(TrackingState trackingState, TDataItem currentValue, TDataItem? originalValue = null) : TrackingItem<TDataItem, Guid>(trackingState, currentValue, originalValue) //public class TrackingItemGuid<TDataItem>(TrackingState trackingState, TDataItem currentValue, TDataItem? originalValue = null) : TrackingItem<TDataItem, Guid>(trackingState, currentValue, originalValue)
// where TDataItem : class, IId<Guid> { } // where TDataItem : class, IId<Guid> { }

View File

@ -21,7 +21,7 @@ Custom binary SignalR protocol, client infrastructure, message tagging, and seri
### Message Tagging ### Message Tagging
- **`SignalMessageTagAttribute.cs`** — Three attributes: `TagAttribute` (base, int messageTag), `SignalRAttribute` (server method routing + client notification), `SignalRSendToClientAttribute` (client-side receive). - **`SignalMessageTagAttribute.cs`** — Three attributes: `TagAttribute` (base, int messageTag), `SignalRAttribute` (server method routing + client notification), `SignalRSendToClientAttribute` (client-side receive).
- **`AcSignalRTags.cs`** — Static constants: `None`, `PingTag`, `EchoTag`. - **`AcSignalRTags.cs`** — Static constants: `None`, `PingTag`, `EchoTag`.
- **`SignalRCrudTags.cs`** — Sealed class bundling 5 independent CRUD tag integers. `GetMessageTagByTrackingState()` maps `TrackingState` -> tag. See `AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md`. - **`SignalRCrudTags.cs`** — Sealed class bundling 5 independent CRUD tag integers. `GetMessageTagByTrackingState()` maps `TrackingState` -> tag. See `AyCode.Services/docs/SIGNALR_DATASOURCE/README.md`.
- **`SendToClientType.cs`** — Enum: None, Others, Caller, All. - **`SendToClientType.cs`** — Enum: None, Others, Caller, All.
### Serialization & Pooling ### Serialization & Pooling

View File

@ -1,7 +1,7 @@
using System.Reflection; using System.Reflection;
using AyCode.Core.Extensions; using AyCode.Core.Extensions;
namespace AyCode.Services.Server.SignalRs; namespace AyCode.Services.SignalRs;
public static class TrackingItemHelpers public static class TrackingItemHelpers
{ {

View File

@ -7,6 +7,7 @@ Topic docs for the `AyCode.Services` project (Layer 0, service abstractions).
- [`LOGGING/`](LOGGING/README.md) — Remote logger (variant — sends log entries over the wire) - [`LOGGING/`](LOGGING/README.md) — Remote logger (variant — sends log entries over the wire)
- [`SIGNALR/`](SIGNALR/README.md) — SignalR transport (tag-based protocol, generic hub methods) - [`SIGNALR/`](SIGNALR/README.md) — SignalR transport (tag-based protocol, generic hub methods)
- [`SIGNALR_BINARY_PROTOCOL/`](SIGNALR_BINARY_PROTOCOL/README.md) — Binary-over-SignalR wire format, chunked framing - [`SIGNALR_BINARY_PROTOCOL/`](SIGNALR_BINARY_PROTOCOL/README.md) — Binary-over-SignalR wire format, chunked framing
- [`SIGNALR_DATASOURCE/`](SIGNALR_DATASOURCE/README.md) — Client-server DataSource on SignalR transport (change tracking, rollback, sync state, IList<T> wrapper)
- [`MVC/`](MVC/README.md) — ASP.NET Core MVC formatters for the AcBinary wire format (InputFormatter / OutputFormatter, `application/vnd.acbinary`) - [`MVC/`](MVC/README.md) — ASP.NET Core MVC formatters for the AcBinary wire format (InputFormatter / OutputFormatter, `application/vnd.acbinary`)
## Navigation ## Navigation
@ -18,5 +19,4 @@ Per the folder-navigation rule, start here when browsing `docs/`. Each topic fol
- **Base logger** (framework): `../../AyCode.Core/AyCode.Core/docs/LOGGING/README.md` - **Base logger** (framework): `../../AyCode.Core/AyCode.Core/docs/LOGGING/README.md`
- **Server-side logger** (variant): `../../AyCode.Core.Server/docs/LOGGING/README.md` - **Server-side logger** (variant): `../../AyCode.Core.Server/docs/LOGGING/README.md`
- **Server-side SignalR**: `../../AyCode.Services.Server/docs/SIGNALR/README.md` - **Server-side SignalR**: `../../AyCode.Services.Server/docs/SIGNALR/README.md`
- **Server-side SignalR DataSource**: `../../AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md`
- **Binary serializer** (used by SIGNALR_BINARY_PROTOCOL): `../../AyCode.Core/AyCode.Core/docs/BINARY/README.md` - **Binary serializer** (used by SIGNALR_BINARY_PROTOCOL): `../../AyCode.Core/AyCode.Core/docs/BINARY/README.md`

View File

@ -3,7 +3,7 @@
Client-side SignalR transport: custom binary protocol, tag-based dispatch. Source: `SignalRs/` Client-side SignalR transport: custom binary protocol, tag-based dispatch. Source: `SignalRs/`
> Server-side hub, session, broadcast: `AyCode.Services.Server/docs/SIGNALR/README.md` > Server-side hub, session, broadcast: `AyCode.Services.Server/docs/SIGNALR/README.md`
> DataSource collection: `AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md` > DataSource collection: `../SIGNALR_DATASOURCE/README.md`
## Design ## Design

File diff suppressed because one or more lines are too long

View File

@ -3,6 +3,31 @@
For planned/actionable work see `SIGNALR_BINARY_PROTOCOL_TODO.md`. For planned/actionable work see `SIGNALR_BINARY_PROTOCOL_TODO.md`.
For higher-level SignalR abstractions see `../SIGNALR/SIGNALR_ISSUES.md`. For higher-level SignalR abstractions see `../SIGNALR/SIGNALR_ISSUES.md`.
## ACCORE-SBP-I-T3W9: AsyncSegment blocking waits on thread-pool → starvation deadlock under concurrent load
**Severity:** Critical · **Status:** Partially Fixed (deser-side fixed 2026-06-08; send-side TBD) · **Area:** `AcBinaryHubProtocol` AsyncSegment send + receive paths
### Description
In `AsyncSegment` mode both directions do **synchronous blocking waits on .NET thread-pool threads**, which starve the pool under concurrent load + real network latency (production-only; never reproduces locally where there is no network and one user). Symptom: clients stuck loading → ~60 s timeout. Hits **all** client types (incl. WASM, which has no `.Wait` of its own — they are victims of server-side starvation).
- **Receive (deser):** `TryParseChunkData` runs the streaming deser via `Task.Run`, which blocks in `AsyncPipeReaderInput.TryAdvanceSegment``ManualResetEventSlim.Wait()` between network-delivered chunks. The unblocking `Feed()`+`Set()` runs on the SignalR receive-continuation — **also** a pool thread → circular dependency → pool starvation, broken only by the runtime's slow thread injection (~1-2/s).
- **Send (flush):** `WriteMessageChunked``SyncFlush(pipeWriter.FlushAsync())` (lines ~535, ~1135) blocks a dispatch (pool) thread **per chunk** under client backpressure (slow mobile clients) while streaming multi-MB responses. Forced by the synchronous `IHubProtocol.WriteMessage` API. **This is the dominant production trigger** (proven by the WASM-client hang + multi-MB server→client payloads).
`Bytes` mode avoids both (no background deser, no per-chunk sync flush) — hence stable as the current hotfix.
### Fix — deser (receive) side ✅ Fixed (2026-06-08)
Resolved — `TryParseChunkData` runs the blocking streaming deser off the .NET thread pool via `AcBinaryDeserExecutor` (`AyCode.Services/SignalRs/AcBinaryDeserExecutor.cs`). See that file's doc-comment for the design.
### Fix — send (flush) side
**TBD.** The per-chunk `SyncFlush` blocking is partly inherent to streaming-with-bounded-memory under the synchronous `WriteMessage` API. To be designed; entry updated when settled.
### Related TODO
(To be opened when the deser-side fix lands and the send-side direction is committed.)
## ACCORE-SBP-I-F6T2: AsyncSegment send-path unsupported on WebAssembly ## ACCORE-SBP-I-F6T2: AsyncSegment send-path unsupported on WebAssembly
**Severity:** Major (on WASM) · **Status:** Open · **Area:** `AsyncPipeWriterOutput` / WASM runtime **Severity:** Major (on WASM) · **Status:** Open · **Area:** `AsyncPipeWriterOutput` / WASM runtime

View File

@ -20,16 +20,19 @@ flag (line ~88) that gates the background deserialize task at line ~936:
```csharp ```csharp
if (state.DeserTask == null && !IsBrowser) if (state.DeserTask == null && !IsBrowser)
{ {
state.DeserTask = Task.Run(() => AcBinaryDeserializer.Deserialize(input2, type, opts)); state.DeserTask = AcBinaryDeserExecutor.Run(() => AcBinaryDeserializer.Deserialize(input2, type, opts));
} }
``` ```
**Non-browser** (server / Windows-app): the deser-task runs on a real thread. **Non-browser** (server / Windows-app): the deser-task runs on a dedicated
off-pool thread via `AcBinaryDeserExecutor` (NOT `Task.Run` / the .NET thread
pool — the blocking `ManualResetEventSlim.Wait()` between chunks would starve
the pool, see `SIGNALR_BINARY_PROTOCOL_ISSUES.md#accore-sbp-i-t3w9`).
`AsyncPipeReaderInput.TryAdvanceSegment(...)` blocks on `AsyncPipeReaderInput.TryAdvanceSegment(...)` blocks on
`ManualResetEventSlim.Wait()` when out of bytes — true pipeline parallelism `ManualResetEventSlim.Wait()` when out of bytes — true pipeline parallelism
between the producer (WebSocket-receive) and consumer (deserializer). between the producer (WebSocket-receive) and consumer (deserializer).
**Browser (WASM single-thread)**: `Task.Run` is skipped. Chunks accumulate in **Browser (WASM single-thread)**: the background deser is skipped. Chunks accumulate in
`AsyncPipeReaderInput` from the WebSocket-receive callback. On `CHUNK_END`, `AsyncPipeReaderInput` from the WebSocket-receive callback. On `CHUNK_END`,
`Input.Complete()` is called → `_completed = true`. The deserializer then `Input.Complete()` is called → `_completed = true`. The deserializer then
runs **synchronously on the current thread** over the fully-buffered payload. runs **synchronously on the current thread** over the fully-buffered payload.

View File

@ -1,9 +1,9 @@
# SignalR DataSource # SignalR DataSource
Change-tracked real-time collection on top of the SignalR transport layer. Source: `SignalRs/AcSignalRDataSource.cs` in `AyCode.Services.Server`. Change-tracked real-time collection on top of the SignalR transport layer. Source: `SignalRs/AcSignalRDataSource.cs` in `AyCode.Services`.
> For the underlying transport (tag system, wire protocol, client base) see `AyCode.Services/docs/SIGNALR/README.md`. > For the underlying transport (tag system, wire protocol, client base) see `AyCode.Services/docs/SIGNALR/README.md`.
> For server hub infrastructure see `../SIGNALR/README.md`. > For server hub infrastructure see `AyCode.Services.Server/docs/SIGNALR/README.md`.
## Overview ## Overview
@ -253,7 +253,7 @@ Projects can also call the transport directly without DataSource — see `AyCode
| Component | Path | | Component | Path |
|---|---| |---|---|
| DataSource (abstract base) | `SignalRs/AcSignalRDataSource.cs` (in `AyCode.Services.Server`) | | DataSource (abstract base) | `SignalRs/AcSignalRDataSource.cs` (in `AyCode.Services`) |
| Tracking helpers (`JsonClone` / `ReflectionClone`) | `SignalRs/TrackingItemHelpers.cs` (in `AyCode.Services.Server`) | | Tracking helpers (`JsonClone` / `ReflectionClone`) | `SignalRs/TrackingItemHelpers.cs` (in `AyCode.Services`) |
| CRUD tags | `SignalRs/SignalRCrudTags.cs` (in `AyCode.Services`) | | CRUD tags | `SignalRs/SignalRCrudTags.cs` (in `AyCode.Services`) |
| Transport client | `SignalRs/AcSignalRClientBase.cs` (in `AyCode.Services`) | | Transport client | `SignalRs/AcSignalRClientBase.cs` (in `AyCode.Services`) |

File diff suppressed because one or more lines are too long

View File

@ -76,7 +76,7 @@ For full architecture see `AyCode.Services/docs/SIGNALR/README.md`.
| **SignalParams** | Metadata accompanying each message (separate hub arg). Carries Status, response type, packed parameters, raw-bytes flag. `[AcBinarySerializable]`. Full spec: `AyCode.Services/docs/SIGNALR/README.md`. | | **SignalParams** | Metadata accompanying each message (separate hub arg). Carries Status, response type, packed parameters, raw-bytes flag. `[AcBinarySerializable]`. Full spec: `AyCode.Services/docs/SIGNALR/README.md`. |
| **Message Tag** | Integer identifier mapping to a method via `[SignalR(tag)]` or `[SignalRSendToClient(tag)]` attributes. | | **Message Tag** | Integer identifier mapping to a method via `[SignalR(tag)]` or `[SignalRSendToClient(tag)]` attributes. |
| **DynamicMethodRegistry** | Resolves message tags to `MethodInfo` at runtime. Static `ConcurrentDictionary` cache with lazy scan on miss. | | **DynamicMethodRegistry** | Resolves message tags to `MethodInfo` at runtime. Static `ConcurrentDictionary` cache with lazy scan on miss. |
| **SignalRCrudTags** | Sealed class bundling 5 independent tag integers (getAllTag, getItemTag, addTag, updateTag, removeTag) for entity CRUD. See `AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md`. | | **SignalRCrudTags** | Sealed class bundling 5 independent tag integers (getAllTag, getItemTag, addTag, updateTag, removeTag) for entity CRUD. See `AyCode.Services/docs/SIGNALR_DATASOURCE/README.md`. |
| **AcBinaryHubProtocol** | Unsealed base `IHubProtocol` replacing SignalR's JSON+Base64 with `AcBinarySerializer`. Protocol name: `"acbinary"`. Full spec (write/read paths, chunked framing): `AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/README.md`. | | **AcBinaryHubProtocol** | Unsealed base `IHubProtocol` replacing SignalR's JSON+Base64 with `AcBinarySerializer`. Protocol name: `"acbinary"`. Full spec (write/read paths, chunked framing): `AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/README.md`. |
| **AyCodeBinaryHubProtocol** | Consumer-derived protocol (per-message header with type resolution, `IsRawBytesData`). Registered via `AddAcBinaryProtocol(...)` DI extension on both server and client. Full spec: `AyCode.Services/docs/SIGNALR/README.md`. | | **AyCodeBinaryHubProtocol** | Consumer-derived protocol (per-message header with type resolution, `IsRawBytesData`). Registered via `AddAcBinaryProtocol(...)` DI extension on both server and client. Full spec: `AyCode.Services/docs/SIGNALR/README.md`. |
| **AcBinaryHubProtocolOptions** | Mutable config for protocol registration (SerializerOptions, ProtocolMode, BufferSize, FlushTimeout, etc.). `Validate()` + `Clone()` for DI safety. Full spec: `AyCode.Services/docs/SIGNALR/README.md`. | | **AcBinaryHubProtocolOptions** | Mutable config for protocol registration (SerializerOptions, ProtocolMode, BufferSize, FlushTimeout, etc.). `Validate()` + `Clone()` for DI safety. Full spec: `AyCode.Services/docs/SIGNALR/README.md`. |

View File

@ -12,8 +12,8 @@ Top-level docs for `AyCode.Core` (Layer 0 — core framework).
- `AyCode.Core/docs/` — Logger, Binary serializer (paired topics: LOGGING/, BINARY/) - `AyCode.Core/docs/` — Logger, Binary serializer (paired topics: LOGGING/, BINARY/)
- `AyCode.Core.Server/docs/` — Server-side logger variant (LOGGING/) - `AyCode.Core.Server/docs/` — Server-side logger variant (LOGGING/)
- `AyCode.Services/docs/` — Remote logger variant, SignalR, SignalR binary protocol - `AyCode.Services/docs/` — Remote logger variant, SignalR, SignalR binary protocol, SignalR DataSource (SIGNALR_DATASOURCE/)
- `AyCode.Services.Server/docs/` — Server-side SignalR hub (SIGNALR/), SignalR DataSource (SIGNALR_DATASOURCE/) - `AyCode.Services.Server/docs/` — Server-side SignalR hub (SIGNALR/)
## Navigation ## Navigation