From 8cf07db4229db63dddbb9062886dcb5716058dd4 Mon Sep 17 00:00:00 2001 From: Juan Castro Date: Fri, 9 Oct 2026 15:30:07 -0400 Subject: [PATCH] refactor: carry the revocation on outer.* instead of outer.pubsub.* Eviction runs through `io.in(room).disconnectSockets()`, which goes via the socket.io Redis adapter a cluster shares, so one node handling the event disconnects that user on every node in it. Fanning out to siblings had each of them repeat the same cluster-wide disconnect. The adapter's Redis is per-cluster, so peer clusters still need the hop -- `outer.*` reaches them and skips the sibling fan-out. The local `auth.sessions.revoked` goes with it: `outer.*` fires on the emitting node too, and nothing else subscribed. --- src/backend/clients/event/types.ts | 9 ++++++--- src/backend/services/auth/AuthService.ts | 7 +------ .../services/socket/SocketService.test.ts | 4 ++-- src/backend/services/socket/SocketService.ts | 16 +++++++--------- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/backend/clients/event/types.ts b/src/backend/clients/event/types.ts index 9662810d4..143afc4f0 100644 --- a/src/backend/clients/event/types.ts +++ b/src/backend/clients/event/types.ts @@ -706,10 +706,13 @@ export type EventMap = { * Sessions were revoked for a user. Anything holding one of them open — a * live socket, say — should drop it: `revoked_at` is otherwise only read on * the next handshake. + * + * `outer.*` rather than `outer.pubsub.*` on purpose: the eviction runs + * through the socket.io Redis adapter a cluster shares, so one node + * disconnecting covers every node in it — fanning out to siblings would + * just have each of them repeat the same cluster-wide disconnect. */ - 'auth.sessions.revoked': { user_id: number; session_uids: string[] }; - /** The same revocation, carried to sibling nodes and peer clusters. */ - 'outer.pubsub.auth.sessions.revoked': { + 'outer.auth.sessions.revoked': { user_id: number; session_uids: string[]; }; diff --git a/src/backend/services/auth/AuthService.ts b/src/backend/services/auth/AuthService.ts index 629d0b3aa..2865dc5e1 100644 --- a/src/backend/services/auth/AuthService.ts +++ b/src/backend/services/auth/AuthService.ts @@ -732,13 +732,8 @@ export class AuthService extends PuterService { user_id: revoked.userId, session_uids: revoked.uuids, }; - this.clients.event?.emit('auth.sessions.revoked', payload, {}); // Whoever holds the socket may not be whoever revoked the session. - this.clients.event?.emit( - 'outer.pubsub.auth.sessions.revoked', - payload, - {}, - ); + this.clients.event?.emit('outer.auth.sessions.revoked', payload, {}); } /** diff --git a/src/backend/services/socket/SocketService.test.ts b/src/backend/services/socket/SocketService.test.ts index 25ffb2ea2..7a1419787 100644 --- a/src/backend/services/socket/SocketService.test.ts +++ b/src/backend/services/socket/SocketService.test.ts @@ -671,10 +671,10 @@ describe('SocketService (live socket.io)', () => { const socket = await connect({ auth_token: `Bearer ${evicted.token}` }); expect(socket.connected).toBe(true); - // As the fan-out delivers it: the node holding this socket is not + // As a peer cluster delivers it: the node holding this socket is not // the one that revoked the session. server.clients.event.emit( - 'outer.pubsub.auth.sessions.revoked', + 'outer.auth.sessions.revoked', { user_id: row!.id, session_uids: ['whatever'] }, {}, ); diff --git a/src/backend/services/socket/SocketService.ts b/src/backend/services/socket/SocketService.ts index cc324397b..ba8c6c2c6 100644 --- a/src/backend/services/socket/SocketService.ts +++ b/src/backend/services/socket/SocketService.ts @@ -834,19 +834,17 @@ export class SocketService extends PuterService { }, ); - // Both channels: the local one for the node that did the revoking, - // the fanned-out one for every other node holding its sockets. - for (const key of [ - 'auth.sessions.revoked', - 'outer.pubsub.auth.sessions.revoked', - ] as const) { - this.clients.event.on(key, (_key: string, data: unknown) => { + // Fires on this node and carries to peer clusters; the adapter + // reaches the rest of this one. + this.clients.event.on( + 'outer.auth.sessions.revoked', + (_key: string, data: unknown) => { const { user_id } = data as { user_id: number }; this.#evictUserSockets(user_id).catch((err: unknown) => { console.error('[socket] session eviction failed', err); }); - }); - } + }, + ); this.clients.event.on( 'auth.access-token.revoked',