Repository navigation
fix(core): release the radio lock while waiting for ACKs - #291
Merged
Merged
Conversation
Radio commands were serialized per method call, so a composite command held the lock for everything it awaited. A DM's send_msg_with_retry kept every other command waiting through all of its ACK timeouts (up to ~36 s with three attempts), stalling channel replies, other DMs, scheduled sends, and automatic message fetching. req_regions_sync did the same for each neighbor scope reply. Serialize at the frame instead: wrap CommandHandler.send() on the handler instance, which is where every meshcore command writes its frame and waits for the radio's immediate reply. There is still at most one in-flight companion frame, paced as before, and library methods that call self.send are covered without a proxy. Waits for ACKs and remote responses now run outside the lock. Add MeshCoreBot.radio_session() for short sequences that must not interleave, and use it for region-scoped channel sends so set scope, send, and restore stay together. The per-call lock never guaranteed that: a DM retry loop queued between set_flood_scope and send_chan_msg sent its flood attempts under the channel's scope.
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.
What this changes
The radio command lock now covers one frame and the radio's immediate reply, instead of a whole command call. A DM waiting for its ACK no longer holds up every other radio command.
modules/core.py:_SerializedCommandsis gone._serialize_command_frames()wrapsCommandHandler.send()on the handler instance, which is where every meshcore command writes its frame and waits for the radio's reply (OK, ERROR, MSG_SENT). There's still at most one in-flight companion frame, paced bycommand_min_interval_msas before. Library methods callself.send, so composite commands andmeshcore_cli.next_cmdare covered without a proxy.MeshCoreBot.radio_session()holds the radio across a short sequence of frames that must not interleave. Re-entering from the same task is a no-op.modules/command_manager.py: region-scoped channel sends run set scope → send → restore inside a session.neighbors_discovery.pyanddocs/packet-capture.mdno longer list "stalls bot replies" as a reason scope collection is off by default. The other reason (it rewrites device contact paths) still stands, so the default is unchanged.Why
I found this while chasing #290. The old proxy wrapped each coroutine on
meshcore.commands, so a composite command held the lock for everything it awaited.send_msg_with_retrykept the radio for its whole retry loop, ACK waits included—up to ~36 s with three attempts when nothing comes back. During that time, channel replies, other DMs, scheduled sends, and automatic message fetching all waited. In one live run, a webhook DM sat behind a keyword reply's retry loop for 5–8 s before its first frame went out.req_regions_syncdid the same thing for each neighbor scope reply.The per-call lock also never kept a scoped channel send together.
set_flood_scope,send_chan_msg, and the restore were three separate calls, so a DM retry loop queued between them ran its flood attempts under the channel's region scope. With per-frame locking, stray frames would land in that gap more often, so the sequence now holds the radio explicitly.Testing
tests/unit/test_command_serializer.py. The new tests cover:send_msg_with_retrywaits for an ACK, another command goes out immediately. Against the old proxy, that command waited out the full 1.1 s ACK window.make test: 4618 passed, 11 skipped.make lintand the CI strict mypy step are clean.send_msg_with_retryfix of mine, which doesn't touch the lock.Checklist
devand targetingdevmake testpassesmake lintpasses (ruff + mypy)Frontend lint passes if templates changed (npm run lint:frontend)CHANGELOG.mdupdated under## [Unreleased]if user-visibleConfig changes are reflected inconfig.ini.example(and the minimal/quickstart templates where relevant) — CI validates these withvalidate_config.py --strictNew docs pages are added tonav:inmkdocs.ymlAny new command justifies its airtime and defaults conservatively