mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-10-23 06:12:19 +00:00
fix(core): release the radio lock while waiting for ACKs
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 commit is contained in:
1 parent
1a8f108ac0
commit
563b830bc5
7 files changed
+272
-147
No files matched your search
+32
-29
@@ -5,6 +5,7 @@ Handles all bot commands, keyword matching, and response generation
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import random
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
@@ -1341,37 +1342,47 @@ class CommandManager:
|
||||
scope_to_use,
|
||||
scope_source,
|
||||
)
|
||||
if not scope_is_global and not hasattr(self.bot.meshcore.commands, "set_flood_scope"):
|
||||
scoped = not scope_is_global and hasattr(self.bot.meshcore.commands, "set_flood_scope")
|
||||
if not scope_is_global and not scoped:
|
||||
self.logger.warning(
|
||||
"Regional flood scope %r requested but meshcore.commands.set_flood_scope "
|
||||
"is unavailable; channel message will use device default (often global flood)",
|
||||
scope_to_use,
|
||||
)
|
||||
elif not scope_is_global:
|
||||
_scope_result = await self.bot.meshcore.commands.set_flood_scope(scope_to_use)
|
||||
if _scope_result is None or getattr(_scope_result, "type", None) == "ERROR":
|
||||
self.logger.warning(
|
||||
"set_flood_scope(%s) failed (result=%s); "
|
||||
"message will be sent with current firmware scope",
|
||||
scope_to_use, _scope_result,
|
||||
)
|
||||
|
||||
target = f"{channel} (channel {channel_num})"
|
||||
# Retry on no_event_received: max 2 extra attempts, 2s apart
|
||||
_max_retries = 2
|
||||
for _attempt in range(_max_retries + 1):
|
||||
try:
|
||||
result = await self.bot.meshcore.commands.send_chan_msg(
|
||||
channel_num, content,
|
||||
timestamp=int(timestamp.timestamp()) if timestamp else None,
|
||||
)
|
||||
finally:
|
||||
if not scope_is_global and hasattr(self.bot.meshcore.commands, "set_flood_scope"):
|
||||
_restore_result = await self.bot.meshcore.commands.set_flood_scope("*")
|
||||
if _restore_result is None or getattr(_restore_result, "type", None) == "ERROR":
|
||||
self.logger.warning(
|
||||
"set_flood_scope('*') restore failed (result=%s)", _restore_result
|
||||
)
|
||||
# Hold the radio from set to restore so no other send goes out
|
||||
# under this message's scope.
|
||||
async with self.bot.radio_session() if scoped else contextlib.nullcontext():
|
||||
if scoped:
|
||||
_scope_result = await self.bot.meshcore.commands.set_flood_scope(scope_to_use)
|
||||
if _scope_result is None or getattr(_scope_result, "type", None) == "ERROR":
|
||||
if _attempt == 0:
|
||||
self.logger.warning(
|
||||
"set_flood_scope(%s) failed (result=%s); "
|
||||
"message will be sent with current firmware scope",
|
||||
scope_to_use, _scope_result,
|
||||
)
|
||||
else:
|
||||
self.logger.warning(
|
||||
"set_flood_scope(%s) failed on retry re-apply (result=%s)",
|
||||
scope_to_use, _scope_result,
|
||||
)
|
||||
try:
|
||||
result = await self.bot.meshcore.commands.send_chan_msg(
|
||||
channel_num, content,
|
||||
timestamp=int(timestamp.timestamp()) if timestamp else None,
|
||||
)
|
||||
finally:
|
||||
if scoped:
|
||||
_restore_result = await self.bot.meshcore.commands.set_flood_scope("*")
|
||||
if _restore_result is None or getattr(_restore_result, "type", None) == "ERROR":
|
||||
self.logger.warning(
|
||||
"set_flood_scope('*') restore failed (result=%s)", _restore_result
|
||||
)
|
||||
|
||||
if self._is_no_event_received(result) and _attempt < _max_retries:
|
||||
self.logger.warning(
|
||||
@@ -1379,14 +1390,6 @@ class CommandManager:
|
||||
f"(attempt {_attempt + 1}/{_max_retries + 1}), retrying in 2s"
|
||||
)
|
||||
await asyncio.sleep(2)
|
||||
# Re-apply scope for next attempt
|
||||
if not scope_is_global and hasattr(self.bot.meshcore.commands, "set_flood_scope"):
|
||||
_scope_result = await self.bot.meshcore.commands.set_flood_scope(scope_to_use)
|
||||
if _scope_result is None or getattr(_scope_result, "type", None) == "ERROR":
|
||||
self.logger.warning(
|
||||
"set_flood_scope(%s) failed on retry re-apply (result=%s)",
|
||||
scope_to_use, _scope_result,
|
||||
)
|
||||
continue
|
||||
break
|
||||
|
||||
|
||||
Reference in new issue
Block a user