From b7d1de05ab91150726d83ae427f320a014ddeeec Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:08:30 +0200 Subject: [PATCH] perf: changing permissions on a large group is slow (#32883) When a group change affects its members (new permissions, parent group or default models, or deleting the group), the server makes every member's browser reconnect so it picks up the new permissions without a page reload. To find each member's connections it read through the whole list of connected users again for every single member, so saving a 2,000-member group with 3,000 people online spent almost 9 seconds on this with Redis as the websocket manager, and about 1 second without. It now finds all members' connections in one pass. Changing one permission on a 2,000-member group, time the save spends making members reconnect: | Online users | Redis, before | Redis, after | No Redis, before | No Redis, after | | --- | --- | --- | --- | --- | | 1,000 | 2.8 s | 0.15 s | 0.09 s | <0.01 s | | 3,000 | 8.7 s | 0.28 s | 1.2 s | <0.01 s | --- backend/open_webui/models/groups.py | 11 +++++------ backend/open_webui/socket/main.py | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/backend/open_webui/models/groups.py b/backend/open_webui/models/groups.py index 6597928ce3..7d477f1836 100644 --- a/backend/open_webui/models/groups.py +++ b/backend/open_webui/models/groups.py @@ -253,13 +253,12 @@ async def refresh_group_sessions(user_ids): return # Import lazily to avoid models/socket import cycles. A committed write must not # be reported as failed just because a client has already disconnected. - from open_webui.socket.main import disconnect_user_sessions + from open_webui.socket.main import disconnect_sessions_for_users - for user_id in set(user_ids): - try: - await disconnect_user_sessions(user_id, refresh_access=True) - except Exception: - log.exception('Unable to refresh group access for user %s', user_id) + try: + await disconnect_sessions_for_users(user_ids, refresh_access=True) + except Exception: + log.exception('Unable to refresh group access for %s user(s)', len(user_ids)) async def validate_parent(db, group_id, parent_id): diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index 09937e9171..a535fb0f51 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -440,11 +440,6 @@ def get_session_ids_from_room(room): return list(members) if members else [] -def get_session_ids_by_user_id(user_id: str) -> list[str]: - """Get known session IDs for a user across the local rooms and shared session pool.""" - return get_session_ids_by_user_ids([user_id]) - - def get_session_ids_by_user_ids(user_ids: list[str]) -> list[str]: """Get known session IDs for users across the local rooms and shared session pool.""" if not user_ids: @@ -517,7 +512,14 @@ async def disconnect_user_sessions(user_id: str, *, refresh_access: bool = False The client will automatically reconnect and re-authenticate with fresh data from the database. """ - session_ids = get_session_ids_by_user_id(user_id) + session_ids = await disconnect_sessions_for_users([user_id], refresh_access=refresh_access) + if session_ids: + log.info('Requested disconnect of %s session(s) for user %s', len(session_ids), user_id) + + +async def disconnect_sessions_for_users(user_ids: list[str], *, refresh_access: bool = False) -> list[str]: + """Disconnect all Socket.IO sessions belonging to these users and return their session IDs.""" + session_ids = get_session_ids_by_user_ids(user_ids) for sid in session_ids: if refresh_access: try: @@ -527,10 +529,8 @@ async def disconnect_user_sessions(user_id: str, *, refresh_access: bool = False try: await sio.disconnect(sid) except Exception: - log.exception('Failed to disconnect session %s for user %s', sid, user_id) - - if session_ids: - log.info('Requested disconnect of %s session(s) for user %s', len(session_ids), user_id) + log.exception('Failed to disconnect session %s', sid) + return session_ids @sio.on('usage')