Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions gateway/channel_directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
action="list" and for resolving human-friendly channel names to numeric IDs.
"""

import asyncio
import json
import logging
from datetime import datetime
Expand Down Expand Up @@ -121,7 +122,7 @@ async def build_channel_directory(adapters: Dict[Any, Any]) -> Dict[str, Any]:
for platform, adapter in adapters.items():
try:
if platform == Platform.DISCORD:
platforms["discord"] = _build_discord(adapter)
platforms["discord"] = await asyncio.to_thread(_build_discord, adapter)
elif platform == Platform.SLACK:
platforms["slack"] = await _build_slack(adapter)
except Exception as e:
Expand All @@ -142,7 +143,7 @@ async def build_channel_directory(adapters: Dict[Any, Any]) -> Dict[str, Any]:
or plat_name not in adapter_platform_names
):
continue
platforms[plat_name] = _build_from_sessions(plat_name)
platforms[plat_name] = await asyncio.to_thread(_build_from_sessions, plat_name)

# Include plugin-registered platforms (dynamic enum members aren't in
# Platform.__members__, so the loop above misses them). Same
Expand All @@ -156,7 +157,7 @@ async def build_channel_directory(adapters: Dict[Any, Any]) -> Dict[str, Any]:
and entry.name not in platforms
and entry.name in adapter_platform_names
):
platforms[entry.name] = _build_from_sessions(entry.name)
platforms[entry.name] = await asyncio.to_thread(_build_from_sessions, entry.name)
except Exception:
pass

Expand Down Expand Up @@ -223,7 +224,7 @@ async def _build_slack(adapter) -> List[Dict[str, Any]]:
"""
team_clients = getattr(adapter, "_team_clients", None) or {}
if not team_clients:
return _build_from_sessions("slack")
return await asyncio.to_thread(_build_from_sessions, "slack")

channels: List[Dict[str, Any]] = []
seen_ids: set = set()
Expand Down Expand Up @@ -267,7 +268,7 @@ async def _build_slack(adapter) -> List[Dict[str, Any]]:
continue

# Merge in DM/group entries discovered from session history.
for entry in _build_from_sessions("slack"):
for entry in await asyncio.to_thread(_build_from_sessions, "slack"):
if entry.get("id") not in seen_ids:
channels.append(entry)
seen_ids.add(entry.get("id"))
Expand Down
1 change: 1 addition & 0 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@
"157689911+itsflownium@users.noreply.github.com": "itsflownium",
"dirtyren@users.noreply.github.com": "dirtyren",
"iamgexin@qq.com": "nullptr0807", # PR #60956 salvage (gateway hygiene in-place compaction; #60947)
"embwl0x@users.noreply.github.com": "embwl0x", # PR #60810 salvage (channel directory offload)
"caztronics@yahoo.com": "doncazper",
"30668368+alex107ivanov@users.noreply.github.com": "alex107ivanov",
"210088133+rungmc357@users.noreply.github.com": "rungmc357",
Expand Down
80 changes: 80 additions & 0 deletions tests/gateway/test_channel_directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import asyncio
import json
import os
import threading
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

Expand Down Expand Up @@ -84,6 +85,85 @@ def broken_dump(data, fp, *args, **kwargs):
assert result == previous


class TestBuildChannelDirectoryOffload:
def test_discord_builder_runs_off_event_loop_thread(self, tmp_path):
from gateway.config import Platform

cache_file = tmp_path / "channel_directory.json"
loop_thread = threading.get_ident()
builder_threads = []

def fake_build_discord(_adapter):
builder_threads.append(threading.get_ident())
return []

with patch("gateway.channel_directory._build_discord", side_effect=fake_build_discord), \
patch("gateway.channel_directory.DIRECTORY_PATH", cache_file):
asyncio.run(build_channel_directory({Platform.DISCORD: object()}))

assert builder_threads
assert all(tid != loop_thread for tid in builder_threads)

def test_session_discovery_runs_off_event_loop_thread(self, tmp_path):
from gateway.config import Platform

cache_file = tmp_path / "channel_directory.json"
loop_thread = threading.get_ident()
calls = []

def fake_build_from_sessions(platform_name):
calls.append((platform_name, threading.get_ident()))
return []

with patch("gateway.channel_directory._build_from_sessions", side_effect=fake_build_from_sessions), \
patch("gateway.channel_directory.DIRECTORY_PATH", cache_file):
asyncio.run(build_channel_directory({Platform.TELEGRAM: object()}))

assert [name for name, _ in calls] == ["telegram"]
assert calls[0][1] != loop_thread

def test_plugin_session_discovery_runs_off_event_loop_thread(self, tmp_path):
cache_file = tmp_path / "channel_directory.json"
loop_thread = threading.get_ident()
calls = []
plugin_entry = SimpleNamespace(name="irc")

def fake_build_from_sessions(platform_name):
calls.append((platform_name, threading.get_ident()))
return []

with patch("gateway.channel_directory._build_from_sessions", side_effect=fake_build_from_sessions), \
patch("gateway.channel_directory.DIRECTORY_PATH", cache_file), \
patch(
"gateway.platform_registry.platform_registry.plugin_entries",
return_value=[plugin_entry],
):
asyncio.run(build_channel_directory({"irc": object()}))

assert [name for name, _ in calls] == ["irc"]
assert calls[0][1] != loop_thread

def test_slack_session_merge_runs_off_event_loop_thread(self):
loop_thread = threading.get_ident()
calls = []

class FakeSlackClient:
async def users_conversations(self, **_kwargs):
return {"ok": True, "channels": []}

def fake_build_from_sessions(platform_name):
calls.append((platform_name, threading.get_ident()))
return [{"id": "D1", "name": "Alice", "type": "dm"}]

adapter = SimpleNamespace(_team_clients={"T1": FakeSlackClient()})
with patch("gateway.channel_directory._build_from_sessions", side_effect=fake_build_from_sessions):
channels = asyncio.run(_build_slack(adapter))

assert channels == [{"id": "D1", "name": "Alice", "type": "dm"}]
assert [name for name, _ in calls] == ["slack"]
assert calls[0][1] != loop_thread


class TestResolveChannelName:
def _setup(self, tmp_path, platforms):
cache_file = _write_directory(tmp_path, platforms)
Expand Down
Loading