Skip to content

RawCan: a blocking driver in Transmit stalls every subscription, because _pendingGate is held across it #102

Description

@dborgards

Follow-up to item 1 of #53, which was closed by #100 with this item deliberately not done. Reopening it as its own issue because it stopped being a minor finding once the counter-argument was written down: it is a lock-scope design change with a correctness constraint on both sides, not a one-line narrowing.

What happens

CanBusService.SendWithEchoConfirmAsync holds _pendingGate across _bus.Transmit(...). With a blocking vendor driver, the thread inside Transmit owns the lock for as long as the driver takes, and the adapter RX thread blocks in TryMatchEcho waiting for it. Every subscription stalls with it — including subscriptions that have nothing to do with the frame being sent.

The lock itself only needs to cover the pending-list bookkeeping. That was item 1's original argument and it is still correct as far as it goes.

Why it was refused, and what any fix has to preserve

Narrowing the lock naively breaks the ordering guarantee the FIFO depends on. From the rationale now in CanBusService.cs:

Register and transmit as one atomic step under _pendingGate: this is what makes TryMatchEcho's FIFO order equal actual transmission order rather than mere registration order. Without it, two threads sending byte-identical frames could register in one order but transmit in the other, so the oldest pending entry is not necessarily the oldest sent one — an echo could then confirm the wrong caller, or confirm a send before it was even transmitted (FR-RAW-031).

Three constraints, all of which a replacement must satisfy:

  1. Echo matching must follow transmission order, not registration order. This is the one that kills the naive fix. Two byte-identical concurrent sends are indistinguishable to the matcher except by order.
  2. A synchronous echo delivered inside Transmit must still work. CanKit.Adapter.Virtual in ChannelWorkMode.Echo re-enters the same lock on the same thread; Monitor is reentrant, so TryMatchEcho sees only this thread's own just-registered entry. Any replacement has to keep that path correct — and it is the path most of the test suite runs on.
  3. The dispose race must stay closed. Dispose sets _pendingDisposed under this same lock, so a call cannot register after the sweep has cancelled every pending entry; it gets ObjectDisposedException instead, matching the eager check at the top of SendConfirmed.

Sketch of a fix that could satisfy all three

Take a monotonically increasing sequence number per PendingKey under the lock, release the lock, then transmit; TryMatchEcho matches the lowest unclaimed sequence rather than the head of a list. Transmission order and registration order can then diverge without the matcher caring, because the sequence was assigned before either.

That is a sketch, not a design. The open questions are what happens to a send whose Transmit throws after it took a sequence number (it must not leave a hole that blocks later matches), and whether the reentrant synchronous-echo path still resolves against the right entry when the lock is no longer held during the driver call.

Related coupling, worth solving in the same pass

The note on the cancellation callback in CanBusService.cs records a second consequence of the same design: a caller's SendConfirmed task completes only after the finally unlinks the entry, and that unlink takes _pendingGate. So completing a send is coupled to the lock too, not just matching. #100 removed the worst symptom — Cancel() no longer blocks on an unrelated send's driver call — but the coupling itself follows from holding the lock across Transmit and would go away with it.

Not urgent

No reported failure depends on this. It matters for real hardware adapters with blocking transmit paths, which the current test suite does not exercise — ControllableBus and CanKit.Adapter.Virtual both return promptly. Anyone picking this up should probably add a bus double whose Transmit blocks for a controllable duration first, and assert that an unrelated subscription keeps receiving while it does. That test is worth having even if the lock is never narrowed, because it pins the current behaviour honestly instead of leaving it undocumented.

Refs #53, #100, FR-RAW-031.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: rawcanCanKit.Pro.RawCan — demux, subscriptions, TX-confirmtype: choreBuild, CI, tooling, housekeeping

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions