fix(can.cc): use napi_make_callback to restore microtask draining between onMessage callbacks#161
Merged
Merged
Conversation
…sage callbacks
The N-API migration replaced Nan::Callback::Call() with fn.Call()
(napi_call_function). The NaN version used node::MakeCallback internally,
which runs a microtask checkpoint and fires async hooks after each JS
callback. napi_call_function does neither.
On a busy bus the entire batch of up to MAX_FRAMES_PER_ASYNC_EVENT frames
fired back-to-back with no opportunity for Promise continuations or
process.nextTick callbacks to run. Adapters that queue processing work
asynchronously inside onMessage (e.g. ioBroker.e3oncan) saw their internal
queue fill faster than it drained, producing "Evaluation of messages
overloaded. Counter=400" warnings and cascading "Bad frame" errors.
Fix: replace fn.Call() with napi_make_callback(), using a napi_async_context
created in Start() and destroyed in async_channel_stopped(). This restores
the node::MakeCallback semantics from the NaN era.
Also replace the silent env.IsExceptionPending()+break pattern with
napi_get_and_clear_last_exception + napi_fatal_exception, matching what
Nan::FatalException / Nan::TryCatch did: exceptions from callbacks are
forwarded to Node's uncaughtException handler rather than left pending in the
env (where they would silently corrupt subsequent napi calls, delivering empty
{} objects to listeners and triggering further "bad frame" warnings).
Add test/test-callback_ordering.js to verify that a microtask (Promise and
process.nextTick) scheduled in callback N resolves before callback N+1 fires.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Users of socketcan 4.1.0 (the N-API migration) see two symptoms compared to 4.0.7 (NaN) when using adapters like ioBroker.e3oncan on a busy CAN bus:
Both symptoms disappear immediately on downgrade to 4.0.7.
Root cause
The NaN implementation called
Nan::Callback::Call()which routes throughnode::MakeCallback(). That function wraps every callback in anode::InternalCallbackScopewhose destructor runs a microtask checkpoint (drains Promises andprocess.nextTick) and firesbefore/afterasync hooks.The N-API migration replaced this with
fn.Call()→napi_call_function(), which skips both steps.On a busy bus, the
uv_async_tbatch can contain up toMAX_FRAMES_PER_ASYNC_EVENT = 100frames. Without a microtask checkpoint between callbacks, all 100 fire back-to-back. Any async work the adapter queues insideonMessage(e.g. a resolved Promise that dequeues the next item from a processing queue) accumulates as 100 pending microtasks instead of being interleaved one-per-frame. The processing queue overflows → "overloaded" warning → missed/malformed deliveries → "bad frame".Fix
Replace
fn.Call()withnapi_make_callback()inasync_receiver_ready()— the code path that is called from auv_async_tcallback (i.e. outside a running JS call, exactly the casenapi_make_callbackis designed for). This restoresnode::MakeCallbacksemantics.Secondary fix: replace the silent
env.IsExceptionPending() + breakpattern withnapi_get_and_clear_last_exception+napi_fatal_exception, matching whatNan::FatalException/Nan::TryCatchdid. Without this, a single thrown exception leaves the env in a corrupted state where all subsequentnapi_*calls silently fail, delivering empty{}objects to listeners (the adapter sees them as "bad frames").Test
test/test-callback_ordering.js— sends 30 frames in one batch and verifies that the Promise /process.nextTickscheduled by callback N has resolved before callback N+1 fires. This test would fail deterministically against the oldfn.Call()code.Checklist
🤖 Generated with Claude Code