Skip to content

Commit 28d1bd1

Browse files
yunho-microsoftYunpeng HouYunpeng Hou
authored
Web socket tracker change (#18930)
This PR includes changes: - Move socket tracker to a separate file - Change socket tracker to track all connected sockets - Add unit test for web socket tracker This change is required for cluster draining feature. --------- Co-authored-by: Yunpeng Hou <[email protected]> Co-authored-by: Yunpeng Hou <[email protected]>
1 parent 2ab624d commit 28d1bd1

14 files changed

+229
-103
lines changed

server/routerlicious/packages/lambdas/src/alfred/index.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -612,11 +612,15 @@ export function configureWebSocketServices(
612612
(connectedMessage as any).timestamp = connectedTimestamp;
613613

614614
// Track socket and tokens for this connection
615-
if (socketTracker && claims.jti) {
616-
socketTracker.addSocketForToken(
617-
core.createCompositeTokenId(message.tenantId, message.id, claims.jti),
618-
socket,
619-
);
615+
if (socketTracker) {
616+
if (claims.jti) {
617+
socketTracker.addSocketForToken(
618+
core.createCompositeTokenId(message.tenantId, message.id, claims.jti),
619+
socket,
620+
);
621+
} else {
622+
socketTracker.addSocket(socket);
623+
}
620624
}
621625

622626
// Set up listener to forward signal to clients in the collaboration session when the broadcast-signal endpoint is called

server/routerlicious/packages/routerlicious-base/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@
132132
"ClassDeclaration_RiddlerResources": {
133133
"forwardCompat": false,
134134
"backCompat": false
135+
},
136+
"ClassDeclaration_AlfredResources": {
137+
"forwardCompat": false
138+
},
139+
"InterfaceDeclaration_IAlfredResourcesCustomizations": {
140+
"forwardCompat": false
135141
}
136142
}
137143
}

server/routerlicious/packages/routerlicious-base/src/test/types/validateServerRouterliciousBasePrevious.generated.ts

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ declare function get_old_ClassDeclaration_AlfredResources():
3131
declare function use_current_ClassDeclaration_AlfredResources(
3232
use: TypeOnly<current.AlfredResources>): void;
3333
use_current_ClassDeclaration_AlfredResources(
34+
// @ts-expect-error compatibility expected to be broken
3435
get_old_ClassDeclaration_AlfredResources());
3536

3637
/*
@@ -199,6 +200,7 @@ declare function get_old_InterfaceDeclaration_IAlfredResourcesCustomizations():
199200
declare function use_current_InterfaceDeclaration_IAlfredResourcesCustomizations(
200201
use: TypeOnly<current.IAlfredResourcesCustomizations>): void;
201202
use_current_InterfaceDeclaration_IAlfredResourcesCustomizations(
203+
// @ts-expect-error compatibility expected to be broken
202204
get_old_InterfaceDeclaration_IAlfredResourcesCustomizations());
203205

204206
/*

server/routerlicious/packages/services-core/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@
9494
"InterfaceDeclaration_ICheckpointService": {
9595
"forwardCompat": false,
9696
"backCompat": false
97+
},
98+
"InterfaceDeclaration_IWebSocketTracker": {
99+
"forwardCompat": false
97100
}
98101
}
99102
}

server/routerlicious/packages/services-core/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,6 @@ export { TokenGenerator } from "./token";
154154
export { clientConnectivityStorageId, IUsageData, signalUsageStorageId } from "./usageData";
155155
export { IZookeeperClient, ZookeeperClientConstructor } from "./zookeeper";
156156
export {
157-
IWebSocketTracker,
158157
ITokenRevocationManager,
159158
IRevokedTokenChecker,
160159
ITokenRevocationResponse,
@@ -164,3 +163,4 @@ export {
164163
createCompositeTokenId,
165164
} from "./tokenRevocationManager";
166165
export { IServiceMessageResourceManager } from "./serviceMessage";
166+
export { IWebSocketTracker } from "./webSocketTracker";

server/routerlicious/packages/services-core/src/test/types/validateServerServicesCorePrevious.generated.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2872,6 +2872,7 @@ declare function get_old_InterfaceDeclaration_IWebSocketTracker():
28722872
declare function use_current_InterfaceDeclaration_IWebSocketTracker(
28732873
use: TypeOnly<current.IWebSocketTracker>): void;
28742874
use_current_InterfaceDeclaration_IWebSocketTracker(
2875+
// @ts-expect-error compatibility expected to be broken
28752876
get_old_InterfaceDeclaration_IWebSocketTracker());
28762877

28772878
/*

server/routerlicious/packages/services-core/src/tokenRevocationManager.ts

-18
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,6 @@
33
* Licensed under the MIT License.
44
*/
55
import { NetworkError, INetworkErrorDetails } from "@fluidframework/server-services-client";
6-
import { IWebSocket } from "./http";
7-
8-
/**
9-
* Interface of web socket tracker
10-
* it tracks the mapping of web socket and token used to establish the socket connection
11-
* @internal
12-
*/
13-
export interface IWebSocketTracker {
14-
// Add a socket to internal map
15-
addSocketForToken(compositeTokenId: string, webSocket: IWebSocket);
16-
17-
// Get socket objects from internal map
18-
getSocketsForToken(compositeTokenId: string): IWebSocket[];
19-
20-
// Remove socket from tracking
21-
// Return true if socket is removed, false if socket is not found
22-
removeSocket(socketId: string): boolean;
23-
}
246

257
/**
268
* @internal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*!
2+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
import { IWebSocket } from "./http";
6+
7+
/**
8+
* Interface of web socket tracker
9+
* it tracks the mapping of web socket and token used to establish the socket connection
10+
* @internal
11+
*/
12+
export interface IWebSocketTracker {
13+
// Add a token to socket mapping
14+
addSocketForToken(compositeTokenId: string, webSocket: IWebSocket);
15+
16+
// Get the socket objects with the given token
17+
getSocketsForToken(compositeTokenId: string): IWebSocket[];
18+
19+
// Add a socket to tracking
20+
addSocket(webSocket: IWebSocket);
21+
22+
// Remove a socket from tracking
23+
// Return true if socket is removed, false if socket is not found
24+
removeSocket(socketId: string): boolean;
25+
26+
// Get all tracked socket objects
27+
getAllSockets(): IWebSocket[];
28+
}

server/routerlicious/packages/services-utils/package.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"prettier:fix": "prettier --write . --cache --ignore-path ../../.prettierignore",
2828
"test": "mocha --recursive \"dist/test/*.spec.*js\"",
2929
"test:coverage": "c8 npm test -- --reporter xunit --reporter-option output=nyc/junit-report.xml",
30+
"test:debug": "mocha inspect --recursive \"dist/test/*.spec.*js\"",
3031
"tsc": "tsc",
3132
"typetests:gen": "fluid-type-test-generator",
3233
"typetests:prepare": "flub typetests --dir . --reset --previous --normalize"
@@ -106,6 +107,10 @@
106107
}
107108
},
108109
"typeValidation": {
109-
"broken": {}
110+
"broken": {
111+
"ClassDeclaration_WebSocketTracker": {
112+
"forwardCompat": false
113+
}
114+
}
110115
}
111116
}

server/routerlicious/packages/services-utils/src/index.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,6 @@ export {
5252
export { bindTimeoutContext } from "./timeoutContext";
5353
export { IThrottleConfig, ISimpleThrottleConfig, getThrottleConfig } from "./throttlerConfigs";
5454
export { IThrottleMiddlewareOptions, throttle } from "./throttlerMiddleware";
55-
export {
56-
WebSocketTracker,
57-
DummyTokenRevocationManager,
58-
DummyRevokedTokenChecker,
59-
} from "./tokenRevocationManager";
55+
export { DummyTokenRevocationManager, DummyRevokedTokenChecker } from "./tokenRevocationManager";
6056
export { WinstonLumberjackEngine } from "./winstonLumberjackEngine";
57+
export { WebSocketTracker } from "./webSocketTracker";

server/routerlicious/packages/services-utils/src/test/types/validateServerServicesUtilsPrevious.generated.ts

+1
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ declare function get_old_ClassDeclaration_WebSocketTracker():
319319
declare function use_current_ClassDeclaration_WebSocketTracker(
320320
use: TypeOnly<current.WebSocketTracker>): void;
321321
use_current_ClassDeclaration_WebSocketTracker(
322+
// @ts-expect-error compatibility expected to be broken
322323
get_old_ClassDeclaration_WebSocketTracker());
323324

324325
/*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*!
2+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
import assert from "assert";
7+
import { WebSocketTracker } from "../webSocketTracker";
8+
import { IWebSocket } from "@fluidframework/server-services-core";
9+
10+
describe("WebSocketTracker", () => {
11+
// let webSocketTracker: WebSocketTracker;
12+
let webSocket1: IWebSocket;
13+
let webSocket2: IWebSocket;
14+
15+
beforeEach(() => {
16+
webSocket1 = {
17+
id: "socketId1",
18+
} as IWebSocket;
19+
webSocket2 = {
20+
id: "socketId2",
21+
} as IWebSocket;
22+
});
23+
24+
it("should add and retrieve a socket for a token", () => {
25+
let webSocketTracker: WebSocketTracker = new WebSocketTracker();
26+
const compositeTokenId = "token1";
27+
webSocketTracker.addSocketForToken(compositeTokenId, webSocket1);
28+
29+
const sockets = webSocketTracker.getSocketsForToken(compositeTokenId);
30+
assert.strictEqual(sockets.length, 1), "Should have 1 socket";
31+
assert.strictEqual(sockets[0].id, webSocket1.id, "Socket id should match");
32+
});
33+
34+
it("should work for multiple calls", () => {
35+
let webSocketTracker: WebSocketTracker = new WebSocketTracker();
36+
let webSocket1: IWebSocket = {
37+
id: "socketId1",
38+
} as IWebSocket;
39+
const compositeTokenId1 = "token1";
40+
41+
// Add 1st socket
42+
webSocketTracker.addSocketForToken(compositeTokenId1, webSocket1);
43+
44+
let sockets = webSocketTracker.getSocketsForToken(compositeTokenId1);
45+
assert.strictEqual(sockets.length, 1, "Should have 1 socket");
46+
assert.strictEqual(sockets[0].id, webSocket1.id, "Should be socket id 1");
47+
48+
// Add 2nd socket
49+
const compositeTokenId2 = "token2";
50+
webSocketTracker.addSocketForToken(compositeTokenId2, webSocket2);
51+
52+
// Check results: socket 2 should exist
53+
sockets = webSocketTracker.getSocketsForToken(compositeTokenId2);
54+
assert.strictEqual(sockets.length, 1);
55+
assert.strictEqual(sockets[0].id, webSocket2.id, "Should be socket id 2");
56+
57+
sockets = webSocketTracker.getAllSockets();
58+
assert.strictEqual(sockets.length, 2, "Should have 2 sockets");
59+
60+
// Remove the socket
61+
webSocketTracker.removeSocket(webSocket1.id);
62+
63+
// Check results
64+
// socket 1 should not exist
65+
sockets = webSocketTracker.getSocketsForToken(compositeTokenId1);
66+
assert.strictEqual(sockets.length, 0, "Should be empty");
67+
68+
// socket 2 should exist
69+
sockets = webSocketTracker.getSocketsForToken(compositeTokenId2);
70+
assert.strictEqual(sockets.length, 1);
71+
assert.strictEqual(sockets[0].id, webSocket2.id, "Should be socket id 2");
72+
73+
// Should only have 1 socket
74+
sockets = webSocketTracker.getAllSockets();
75+
assert.strictEqual(sockets.length, 1), "Should only have 1 socket after removal";
76+
});
77+
78+
it("should add socket", () => {
79+
let webSocketTracker: WebSocketTracker = new WebSocketTracker();
80+
webSocketTracker.addSocket(webSocket1);
81+
82+
const sockets = webSocketTracker.getAllSockets();
83+
assert.strictEqual(sockets.length, 1), "Should have 1 socket";
84+
assert.strictEqual(sockets[0].id, webSocket1.id, "Socket id should match");
85+
});
86+
});

server/routerlicious/packages/services-utils/src/tokenRevocationManager.ts

-73
Original file line numberDiff line numberDiff line change
@@ -3,85 +3,12 @@
33
* Licensed under the MIT License.
44
*/
55
import {
6-
IWebSocket,
7-
IWebSocketTracker,
86
ITokenRevocationManager,
97
IRevokedTokenChecker,
108
ITokenRevocationResponse,
119
} from "@fluidframework/server-services-core";
1210
import { NetworkError } from "@fluidframework/server-services-client";
1311

14-
/**
15-
* @internal
16-
*/
17-
export class WebSocketTracker implements IWebSocketTracker {
18-
// Map of socket id to socket object
19-
private readonly socketIdToSocketMap: Map<string, IWebSocket>;
20-
// Map of composite token id to socket ids. It assumes one token could be used for multiple sockets
21-
private readonly tokenIdToSocketIdMap: Map<string, Set<string>>;
22-
// Map of socketId to token ids. It assumes one socket could be used for connections with multiple tokens
23-
private readonly socketIdToTokenIdMap: Map<string, Set<string>>;
24-
25-
constructor() {
26-
this.socketIdToSocketMap = new Map();
27-
this.tokenIdToSocketIdMap = new Map();
28-
this.socketIdToTokenIdMap = new Map();
29-
}
30-
31-
public addSocketForToken(compositeTokenId: string, webSocket: IWebSocket) {
32-
const socketIds = this.tokenIdToSocketIdMap.get(compositeTokenId);
33-
if (socketIds) {
34-
socketIds.add(webSocket.id);
35-
} else {
36-
this.tokenIdToSocketIdMap.set(compositeTokenId, new Set([webSocket.id]));
37-
}
38-
39-
const tokenIds = this.socketIdToTokenIdMap.get(webSocket.id);
40-
if (tokenIds) {
41-
tokenIds.add(compositeTokenId);
42-
} else {
43-
this.socketIdToTokenIdMap.set(webSocket.id, new Set([compositeTokenId]));
44-
}
45-
46-
this.socketIdToSocketMap.set(webSocket.id, webSocket);
47-
}
48-
49-
public getSocketsForToken(compositeTokenId: string): IWebSocket[] {
50-
const socketIds = this.tokenIdToSocketIdMap.get(compositeTokenId);
51-
52-
if (!socketIds) {
53-
return [];
54-
}
55-
56-
const socketResult: IWebSocket[] = [];
57-
socketIds.forEach((socketId: string) => {
58-
const socketObj = this.socketIdToSocketMap.get(socketId);
59-
if (socketObj) {
60-
socketResult.push(socketObj);
61-
}
62-
});
63-
return socketResult;
64-
}
65-
66-
public removeSocket(socketId: string) {
67-
const tokenIds = this.socketIdToTokenIdMap.get(socketId);
68-
69-
if (tokenIds) {
70-
tokenIds.forEach((tokenId: string) => {
71-
const socketIds = this.tokenIdToSocketIdMap.get(tokenId);
72-
if (socketIds) {
73-
socketIds.delete(socketId);
74-
if (socketIds.size <= 0) {
75-
this.tokenIdToSocketIdMap.delete(tokenId);
76-
}
77-
}
78-
});
79-
}
80-
this.socketIdToTokenIdMap.delete(socketId);
81-
return this.socketIdToSocketMap.delete(socketId);
82-
}
83-
}
84-
8512
/**
8613
* @internal
8714
*/

0 commit comments

Comments
 (0)