Skip to content

feat(rawcan): callback-style Subscribe(onNext, predicate) - #15

Merged
dborgards merged 4 commits into
mainfrom
feature/raw-can-callback-subscribe
Sep 9, 2026
Merged

feat(rawcan): callback-style Subscribe(onNext, predicate)#15
dborgards merged 4 commits into
mainfrom
feature/raw-can-callback-subscribe

Conversation

@dborgards

Copy link
Copy Markdown
Owner

Summary

  • Adds CanBusServiceExtensions.Subscribe(onNext, predicate, bufferCapacity): a small, additive-only extension method for subscribers who want a callback instead of driving ISubscription.Frames themselves.
  • Deliberately scoped to raw-CAN only, not a cross-protocol unification -- see commit message for the reasoning (protocol layers already have their own typed event APIs; unifying them would either duplicate for one line of savings or flatten typed payloads to a lowest common denominator).
  • Built entirely on the existing pull API, so it inherits FR-RAW-011 for free: a slow onNext only falls behind and drops its own subscription's oldest frames, never delaying other subscriptions or the bus's own FrameObserved event.
  • A throwing onNext is isolated per frame and routed through the existing BackgroundExceptionOccurred fault channel (small internal CanBusService.RaiseBackgroundException helper added, also de-duplicates the identical inline try/catch OnFrameObserved already had).

Test plan

  • 4 new tests: filter correctness, slow-handler-does-not-block (mirrors the existing raw-subscription FR-RAW-011 test), Dispose stops delivery, handler-exception routed + delivery continues
  • Public API surface changed (new type in CanKit.Pro.RawCan) -- approval baseline updated in the same commit
  • Full dotnet build + dotnet test -- 414/414 passing

…ence API

A colleague asked for a way to subscribe to incoming CAN frames with a
callback instead of driving the ISubscription.Frames async-enumerable
manually. Rather than a bigger cross-protocol unification (the
protocol layers already have their own event-based APIs -- J1939's
MessageReceived, ISO-TP's DatagramReceived, CANopen's PDO/SDO events
-- and unifying those under one generic signature would either
duplicate them for one line of savings or flatten their typed
payloads to a lowest common denominator), this adds a small,
additive-only extension method scoped to the raw-CAN layer, where the
underlying complexity (per-subscription bounded buffering, backpressure)
is real enough to be worth hiding.

CanBusServiceExtensions.Subscribe(onNext, predicate, bufferCapacity)
is built entirely on the existing ISubscription pull API, so it
inherits the same FR-RAW-011 guarantee for free: a slow onNext only
falls behind and drops its own subscription's oldest frames -- it can
never delay delivery to other subscriptions or to the bus's own
FrameObserved event, because the dispatch hot path never waits on a
subscriber's consumer. A throwing onNext is isolated per frame and
routed through the existing BackgroundExceptionOccurred fault channel
via a new small internal CanBusService.RaiseBackgroundException
helper (also used to de-duplicate the identical inline try/catch
OnFrameObserved already had).

Four new tests cover: filtering, the slow-handler-does-not-block
invariant (mirroring the existing raw-subscription FR-RAW-011 test),
Dispose stopping delivery, and handler-exception routing +
delivery continuing. Public API surface changed (new type in
CanKit.Pro.RawCan) -- approval baseline updated in the same commit.

Verified: full build + 414/414 tests green on net10.0.
@cursor

cursor Bot commented Sep 9, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Additive public API and internal fault helper on the raw-CAN subscription layer; behavior is covered by extensive tests and does not change the existing pull subscription contract.

Overview
Adds a callback-style ICanBusService.Subscribe(onNext, predicate, bufferCapacity) extension in CanKit.Pro.RawCan for callers who want filter + handler instead of pumping ISubscription.Frames. Delivery runs on a dedicated background task over the existing pull subscription, so FR-RAW-011 behavior is unchanged: a slow handler only drops its own buffered frames and does not block other subscribers or FrameObserved.

Dispose completes the underlying subscription, uses an AsyncLocal reentrancy guard so self-dispose from onNext does not deadlock on pump join, and checks a disposed flag so queued channel items are not delivered after dispose returns. Handler exceptions are isolated per frame; when the service is CanBusService, they go through BackgroundExceptionOccurred via a new internal RaiseBackgroundException helper that also replaces the duplicated inline invoke in frame dispatch.

Public API approval baseline is updated; tests cover filtering, non-blocking slow handlers, dispose semantics (including in-handler dispose), exception surfacing, and continued delivery.

Reviewed by Cursor Bugbot for commit fa54874. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread tests/CanKit.Pro.Tests/TestCases/RawCanSubscriptionTests.cs Fixed
Comment thread tests/CanKit.Pro.Tests/TestCases/RawCanSubscriptionTests.cs Dismissed
Comment thread src/CanKit.Pro.RawCan/CanBusService.cs Dismissed
Comment thread src/CanKit.Pro.RawCan/CanBusServiceExtensions.cs Fixed
Comment thread src/CanKit.Pro.RawCan/CanBusServiceExtensions.cs Dismissed

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Dispose from callback hangs two seconds
    • CallbackSubscription.Dispose now skips the pump-task join when invoked from onNext, so unsubscribe-from-handler no longer deadlocks until the two-second timeout.

You can send follow-ups to the cloud agent here.

Comment thread src/CanKit.Pro.RawCan/CanBusServiceExtensions.cs
…Next

CallbackSubscription.Dispose waited on the pump task that invokes onNext, so
disposing the handle from inside the callback deadlocked until the two-second
timeout. Skip that join when Dispose runs on the pump, matching ProtocolActor.
Comment thread src/CanKit.Pro.RawCan/CanBusServiceExtensions.cs Fixed
CodeQL flagged it (cs/local-not-disposed) on the PR. Genuine gap, not
a by-design pattern like the rest of this session's CodeQL triage.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Self-dispose still delivers buffered frames
    • The callback pump now checks the disposed flag around onNext so a self-dispose no longer drains remaining buffered frames after Dispose returns.

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit e44f3cc. Configure here.

Comment thread src/CanKit.Pro.RawCan/CanBusServiceExtensions.cs
Completing the subscription channel does not drop already-buffered
frames, so disposing from onNext skipped the pump join and still
delivered the remainder after Dispose returned. Stop the pump on the
disposed flag instead.
Comment thread src/CanKit.Pro.RawCan/CanBusServiceExtensions.cs Dismissed
@dborgards
dborgards merged commit a6686d6 into main Sep 9, 2026
9 checks passed
dborgards added a commit that referenced this pull request Sep 9, 2026
10.4.0 (merged via the routine dependency-bump PR #2) requires
conventional-changelog-writer@9+, but @semantic-release/commit-analyzer,
release-notes-generator and changelog (all pinned to older majors here)
resolve conventional-changelog-writer@^8.0.0 -- so npm installs 8.4.0.
The mismatch doesn't surface at plugin/preset resolution time (which is
all eng/verify-release-config.mjs checks, and all a PR build can check
without push-rights), only when generateNotes actually renders a
changelog template against a real commit history -- which only happens
on a genuine release attempt on main. That's exactly what just failed
after merging PR #15 (a feat: commit, the first release-triggering
commit since #2 landed): "Missing helper: conventional-changelog-writer
requires @9 or newer" (Handlebars helper missing from the older writer).

No release side effects had happened yet (generateNotes runs before
prepare/publish/tag) -- the run failed clean, nothing to unwind.

Reverting to the last known-good 9.3.1 is the safe immediate fix.
Re-attempting the writer@9+ upgrade later needs @semantic-release/
commit-analyzer, release-notes-generator and changelog bumped together
with it, and should be verified with a real
`GITHUB_TOKEN=$(gh auth token) npx semantic-release --dry-run --no-ci`
locally (not just eng/verify-release-config.mjs, which only checks
resolvability, not template rendering) -- that's exactly what confirmed
this fix: generateNotes now completes and prints the real release notes.
dborgards pushed a commit that referenced this pull request Sep 9, 2026
## [1.1.0](v1.0.0...v1.1.0) (2026-09-09)

### Features

* **rawcan:** add callback-style Subscribe(onNext, predicate) convenience API ([f3bb3e8](f3bb3e8))

### Bug Fixes

* **ci:** resolve release-config presets via import.meta.resolve ([e30423d](e30423d))
* **ci:** use a bypass-eligible PAT for the release push ([03e0cd9](03e0cd9))
* **rawcan:** skip pump join when disposing callback Subscribe from onNext ([be23113](be23113))
* **rawcan:** stop callback pump after self-dispose ([fa54874](fa54874))
* **release:** revert conventional-changelog-conventionalcommits to 9.3.1 ([1910026](1910026)), closes [#2](#2) [#15](#15) [#2](#2)

### Build & Dependencies

* **deps:** Bump conventional-changelog-conventionalcommits ([#2](#2)) ([41af4e6](41af4e6))
* **deps:** Bump coverlet.collector and 2 others ([#8](#8)) ([b05ae9c](b05ae9c))
* **deps:** Bump Microsoft.Bcl.AsyncInterfaces from 10.0.11 to 10.0.12 ([#9](#9)) ([67aee2c](67aee2c))
* **deps:** Bump System.Memory from 4.5.4 to 4.6.3 ([#10](#10)) ([c15225a](c15225a))
* **deps:** Bump System.Threading.Channels from 10.0.11 to 10.0.12 ([#11](#11)) ([50db23c](50db23c))
* move to .NET 10 LTS ahead of net8.0 EOL ([cad2962](cad2962))
@dborgards
dborgards deleted the feature/raw-can-callback-subscribe branch September 9, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants