fix(feishu): bound _chat_locks with LRU eviction (#34836)

The Feishu adapter stored one asyncio.Lock per chat_id in a plain dict
with no upper bound, so a long-running gateway that saw many distinct
chats grew _chat_locks without limit. Port the LRU-eviction pattern
already used by the yuanbao adapter: OrderedDict + move_to_end on access,
CHAT_LOCK_MAX_SIZE cap (1000), and eviction that skips currently-held
locks (falling back to dropping the LRU entry only if all are held).
This commit is contained in:
Teknium
2026-05-29 13:18:15 -07:00
committed by GitHub
parent 8055d0f092
commit 91b174038c
2 changed files with 84 additions and 5 deletions
+59
View File
@@ -4883,3 +4883,62 @@ class TestFeishuMentionEndToEnd(unittest.TestCase):
# Body: leading @Hermes stripped, Alice preserved, trailing text intact.
self.assertIn("@Alice review the spec with Alice", event.text)
self.assertNotIn("@Hermes @Alice", event.text)
class TestChatLockEviction(unittest.TestCase):
"""_get_chat_lock is LRU-bounded so _chat_locks cannot grow unbounded."""
def _make_adapter(self, max_size=5):
import collections as _collections
from gateway.platforms.feishu import FeishuAdapter
adapter = object.__new__(FeishuAdapter)
adapter._chat_locks = _collections.OrderedDict()
adapter.CHAT_LOCK_MAX_SIZE = max_size
return adapter
def test_chat_locks_is_ordered_dict(self):
import collections as _collections
adapter = self._make_adapter()
self.assertIsInstance(adapter._chat_locks, _collections.OrderedDict)
def test_same_id_returns_same_lock_and_stays_bounded(self):
adapter = self._make_adapter(max_size=5)
locks = [adapter._get_chat_lock(f"c{i}") for i in range(5)]
self.assertEqual(len(adapter._chat_locks), 5)
# Re-requesting an existing id returns the identical lock, no growth.
self.assertIs(adapter._get_chat_lock("c2"), locks[2])
self.assertEqual(len(adapter._chat_locks), 5)
def test_lru_eviction_respects_recent_access(self):
adapter = self._make_adapter(max_size=5)
for i in range(5):
adapter._get_chat_lock(f"c{i}")
# Touch c0 so it is no longer the LRU entry, then add a new chat.
adapter._get_chat_lock("c0")
adapter._get_chat_lock("c_new")
self.assertEqual(len(adapter._chat_locks), 5)
self.assertNotIn("c1", adapter._chat_locks) # c1 was the true LRU
self.assertIn("c0", adapter._chat_locks)
self.assertIn("c_new", adapter._chat_locks)
def test_eviction_skips_held_locks(self):
adapter = self._make_adapter(max_size=3)
async def _run():
held = adapter._get_chat_lock("held")
await held.acquire()
try:
adapter._get_chat_lock("x")
adapter._get_chat_lock("y")
# At capacity; "held" is LRU but locked, so "x" should go instead.
adapter._get_chat_lock("z")
self.assertIn("held", adapter._chat_locks)
self.assertNotIn("x", adapter._chat_locks)
self.assertEqual(len(adapter._chat_locks), 3)
finally:
held.release()
asyncio.run(_run())