fix(buzz): scoped identity lock, negative name caching, sidebar registration

Follow-up on the salvaged #71610 commits:
- acquire/release a scoped lock on relay_url+pubkey in connect/disconnect
  (IRC pattern) so two profiles can't drive one Buzz identity — duplicate
  replies and split de-dupe state; +2 tests
- negative-cache _resolve_user_name failures so a profile-less pubkey
  doesn't re-hit 'users get' every poll sweep (flagged by @jethac on the PR)
- register user-guide/messaging/buzz in website/sidebars.ts (page was
  unreachable — the #63359 trap)
This commit is contained in:
Teknium 2026-07-28 12:31:15 -07:00
parent e01d6650fe
commit 1b9377b1fd
4 changed files with 78 additions and 2 deletions

View file

@ -0,0 +1 @@
rob-coco

View file

@ -360,6 +360,7 @@ class BuzzAdapter(BasePlatformAdapter):
# Runtime state
self._poll_task: Optional[asyncio.Task] = None
self._lock_key: Optional[str] = None
# channel_id -> {"chat_type", "last_ts", "seen": OrderedDict[event_id, None]}
self._channel_state: Dict[str, dict] = {}
self._channel_names: Dict[str, str] = {}
@ -421,6 +422,27 @@ class BuzzAdapter(BasePlatformAdapter):
self._display_name = str(profiles[0].get("display_name") or "").strip()
self._self_npub = hex_to_npub(self._self_pubkey) or ""
# Prevent two profiles from driving the same Buzz identity on the
# same relay (duplicate replies, split de-dupe state). Mirrors the
# IRC adapter's scoped-lock pattern.
try:
from gateway.status import acquire_scoped_lock
lock_key = f"{self.relay_url}:{self._self_pubkey}"
if not acquire_scoped_lock("buzz", lock_key):
logger.error(
"Buzz: identity %s… on %s already in use by another profile",
self._self_pubkey[:8],
self.relay_url,
)
self._set_fatal_error(
"lock_conflict", "Buzz identity in use by another profile", retryable=False
)
return False
self._lock_key = lock_key
except ImportError:
self._lock_key = None # status module not available (e.g. tests)
# Map channel ids to names and pick the watch set.
code, out, err = await self._run_cli(["channels", "list"])
if code != 0:
@ -463,6 +485,15 @@ class BuzzAdapter(BasePlatformAdapter):
async def disconnect(self) -> None:
"""Stop the poll loop and drop runtime state."""
self._mark_disconnected()
lock_key = getattr(self, "_lock_key", None)
if lock_key:
try:
from gateway.status import release_scoped_lock
release_scoped_lock("buzz", lock_key)
except Exception:
pass
self._lock_key = None
if self._poll_task and not self._poll_task.done():
self._poll_task.cancel()
try:
@ -874,9 +905,15 @@ class BuzzAdapter(BasePlatformAdapter):
return stripped.strip()
async def _resolve_user_name(self, pubkey: str) -> str:
"""Resolve a pubkey to a display name (cached; falls back to npub prefix)."""
"""Resolve a pubkey to a display name (cached; falls back to npub prefix).
Failures are cached too (negative caching): without it, every message
from a profile-less pubkey re-runs ``users get`` each poll sweep,
which amplifies badly when several adapter instances poll in one
process.
"""
cached = self._user_names.get(pubkey)
if cached:
if cached is not None:
return cached
name = ""
code, out, _err = await self._run_cli(["users", "get", "--pubkey", pubkey])

View file

@ -704,6 +704,43 @@ class TestBuzzAdapterLifecycle:
assert adapter._poll_task is None
assert adapter.is_connected is False
@pytest.mark.asyncio
async def test_disconnect_releases_scoped_lock(self, monkeypatch):
"""The identity lock taken in connect() must be released on disconnect."""
import gateway.status as gateway_status
released = []
monkeypatch.setattr(
gateway_status,
"release_scoped_lock",
lambda platform, key: released.append((platform, key)),
)
adapter = _make_adapter()
adapter._lock_key = "wss://relay.example:" + SELF_PUBKEY
await adapter.disconnect()
assert released == [("buzz", "wss://relay.example:" + SELF_PUBKEY)]
assert adapter._lock_key is None
@pytest.mark.asyncio
async def test_connect_fails_when_identity_lock_held(self, monkeypatch):
"""A second profile using the same relay+pubkey must fail fast."""
import gateway.status as gateway_status
monkeypatch.setattr(
gateway_status, "acquire_scoped_lock", lambda platform, key: False
)
adapter = _make_adapter()
adapter.cli_path = "/fake/buzz"
monkeypatch.setattr(_buzz_mod, "_resolve_private_key", lambda extra=None: "nsec1test")
cli = _ScriptedCli()
cli.script(
"users", "get",
[{"pubkey": SELF_PUBKEY, "display_name": "Chip"}],
)
adapter._run_cli = cli
assert await adapter.connect() is False
assert adapter._lock_key is None
@pytest.mark.asyncio
async def test_get_chat_info_uses_cached_names(self):
adapter = _make_adapter()

View file

@ -656,6 +656,7 @@ const sidebars: SidebarsConfig = {
'user-guide/messaging/mattermost',
'user-guide/messaging/matrix',
'user-guide/messaging/bluebubbles',
'user-guide/messaging/buzz',
'user-guide/messaging/photon',
'user-guide/messaging/google_chat',
'user-guide/messaging/line',