diff --git a/changelog.d/893.feature b/changelog.d/893.feature new file mode 100644 index 000000000..4c88f71a5 --- /dev/null +++ b/changelog.d/893.feature @@ -0,0 +1 @@ +Improve startup time on redis configurations by caching the last known active rooms. diff --git a/src/Bridge.ts b/src/Bridge.ts index c92517458..d20426a4c 100644 --- a/src/Bridge.ts +++ b/src/Bridge.ts @@ -684,12 +684,20 @@ export class Bridge { const queue = new PQueue({ concurrency: 2, }); + // Set up already joined rooms - await queue.addAll(this.botUsersManager.joinedRooms.map((roomId) => async () => { + let allActiveJoinedRooms = await this.storage.getAllRoomsWithActiveConnections(); + if (!allActiveJoinedRooms.length) { + allActiveJoinedRooms = this.botUsersManager.joinedRooms; + } + log.info(`Found ${allActiveJoinedRooms.length} active rooms`); + + const loadRoom = async (roomId: string) => { log.debug("Fetching state for " + roomId); try { await connManager.createConnectionsForRoomId(roomId, false); + this.storage.addRoomHasActiveConnections(roomId); } catch (ex) { log.error(`Unable to create connection for ${roomId}`, ex); return; @@ -735,13 +743,16 @@ export class Bridge { } } const adminRoom = await this.setUpAdminRoom(botUser.intent, roomId, accountData, notifContent || NotifFilter.getDefaultContent()); + this.storage.addRoomHasActiveConnections(roomId); // Call this on startup to set the state await this.onAdminRoomSettingsChanged(adminRoom, accountData, { admin_user: accountData.admin_user }); log.debug(`Room ${roomId} is connected to: ${adminRoom.toString()}`); } catch (ex) { log.error(`Failed to set up admin room ${roomId}:`, ex); } - })); + } + + await queue.addAll(allActiveJoinedRooms.map(roomId => () => loadRoom(roomId))); // Handle spaces for (const discussion of connManager.getAllConnectionsOfType(GitHubDiscussionSpace)) { @@ -774,8 +785,9 @@ export class Bridge { if (this.config.metrics?.enabled) { this.listener.bindResource('metrics', Metrics.expressRouter); } + // This will load all the *active* connections await queue.onIdle(); - log.info(`All connections loaded`); + log.info(`All active connections loaded`); // Load feeds after connections, to limit the chances of us double // posting to rooms if a previous hookshot instance is being replaced. @@ -794,6 +806,9 @@ export class Bridge { await this.as.begin(); log.info(`Bridge is now ready. Found ${this.connectionManager.size} connections`); this.ready = true; + const inactiveRooms = this.botUsersManager.joinedRooms.filter(rId => !allActiveJoinedRooms.includes(rId)); + log.info(`Checking ${inactiveRooms.length} rooms with previously inactive state in the background`); + await queue.addAll(inactiveRooms.map(rId => () => loadRoom(rId))); } private async handleHookshotEvent(msg: MessageQueueMessageOut, connection: ConnType, handler: (c: ConnType, data: EventType) => Promise|unknown) { @@ -1384,6 +1399,7 @@ export class Bridge { const adminRoom = new AdminRoom( roomId, accountData, notifContent, intent, this.tokenStore, this.config, this.connectionManager, ); + this.storage.addRoomHasActiveConnections(roomId); adminRoom.on("settings.changed", this.onAdminRoomSettingsChanged.bind(this)); adminRoom.on("open.project", async (project: ProjectsGetResponseData) => { diff --git a/src/ConnectionManager.ts b/src/ConnectionManager.ts index c475aa778..9cbb54c0a 100644 --- a/src/ConnectionManager.ts +++ b/src/ConnectionManager.ts @@ -61,6 +61,7 @@ export class ConnectionManager extends EventEmitter { } this.connections.push(connection); this.emit('new-connection', connection); + this.storage.addRoomHasActiveConnections(connection.roomId); } Metrics.connections.set(this.connections.length); // Already exists, noop. @@ -394,6 +395,9 @@ export class ConnectionManager extends EventEmitter { this.connections.splice(connectionIndex, 1); Metrics.connections.set(this.connections.length); this.emit('connection-removed', connection); + if (this.getAllConnectionsForRoom(roomId).length === 0) { + this.storage.removeRoomHasActiveConnections(roomId); + } } /** @@ -403,8 +407,13 @@ export class ConnectionManager extends EventEmitter { */ public async removeConnectionsForRoom(roomId: string) { log.info(`Removing all connections from ${roomId}`); - this.connections = this.connections.filter((c) => c.roomId !== roomId); + const removedConnections = this.connections.filter((c) => c.roomId === roomId); + this.connections = this.connections.filter((c) => !removedConnections.includes(c)); + removedConnections.forEach(c => { + this.emit('connection-removed', c); + }) Metrics.connections.set(this.connections.length); + this.storage.removeRoomHasActiveConnections(roomId); } public registerProvisioningConnection(connType: {getProvisionerDetails: (botUserId: string) => GetConnectionTypeResponseItem}) { diff --git a/src/Stores/MemoryStorageProvider.ts b/src/Stores/MemoryStorageProvider.ts index 257c1da3c..5b1a2e4e2 100644 --- a/src/Stores/MemoryStorageProvider.ts +++ b/src/Stores/MemoryStorageProvider.ts @@ -107,4 +107,14 @@ export class MemoryStorageProvider extends MSP implements IBridgeStorageProvider public async setGitlabDiscussionThreads(connectionId: string, value: SerializedGitlabDiscussionThreads): Promise { this.gitlabDiscussionThreads.set(connectionId, value); } + public addRoomHasActiveConnections(): void { + // no-op: only used for startup speedups + } + public removeRoomHasActiveConnections(): void { + // no-op: only used for startup speedups + } + public async getAllRoomsWithActiveConnections(): Promise { + // no-op: only used for startup speedups + return []; + } } diff --git a/src/Stores/RedisStorageProvider.ts b/src/Stores/RedisStorageProvider.ts index 4f2343ce5..7e36e60fd 100644 --- a/src/Stores/RedisStorageProvider.ts +++ b/src/Stores/RedisStorageProvider.ts @@ -18,19 +18,15 @@ const GH_ISSUES_REVIEW_DATA_KEY = "gh.issues.review_data"; const FIGMA_EVENT_COMMENT_ID = "figma.comment_event_id"; const STORED_FILES_KEY = "storedfiles."; const GL_DISCUSSIONTHREADS_KEY = "gl.discussion-threads"; +const ACTIVE_ROOMS = "cache.active_rooms"; const STORED_FILES_EXPIRE_AFTER = 24 * 60 * 60; // 24 hours const COMPLETED_TRANSACTIONS_EXPIRE_AFTER = 24 * 60 * 60; // 24 hours const ISSUES_EXPIRE_AFTER = 7 * 24 * 60 * 60; // 7 days const ISSUES_LAST_COMMENT_EXPIRE_AFTER = 14 * 24 * 60 * 60; // 7 days - - const WIDGET_TOKENS = "widgets.tokens."; const WIDGET_USER_TOKENS = "widgets.user-tokens."; - const FEED_GUIDS = "feeds.guids."; - - const log = new Logger("RedisASProvider"); export class RedisStorageContextualProvider implements IStorageProvider { @@ -229,4 +225,20 @@ export class RedisStorageProvider extends RedisStorageContextualProvider impleme public async hasSeenFeedGuid(url: string, guid: string): Promise { return (await this.redis.lpos(`${FEED_GUIDS}${url}`, guid)) != null; } + + public addRoomHasActiveConnections(roomId: string): void { + this.redis.sadd(ACTIVE_ROOMS, roomId).catch((ex) => { + log.warn(`Failed to add ${roomId} to active rooms`, ex); + }); + } + + public removeRoomHasActiveConnections(roomId: string): void { + this.redis.srem(ACTIVE_ROOMS, roomId).catch((ex) => { + log.warn(`Failed to remove ${roomId} from active rooms`, ex); + }); + } + + public getAllRoomsWithActiveConnections(): Promise { + return this.redis.smembers(ACTIVE_ROOMS); + } } diff --git a/src/Stores/StorageProvider.ts b/src/Stores/StorageProvider.ts index 73790ff95..4e86f4008 100644 --- a/src/Stores/StorageProvider.ts +++ b/src/Stores/StorageProvider.ts @@ -28,4 +28,7 @@ export interface IBridgeStorageProvider extends IAppserviceStorageProvider, ISto storeFeedGuids(url: string, ...guid: string[]): Promise; hasSeenFeed(url: string, ...guid: string[]): Promise; hasSeenFeedGuid(url: string, guid: string): Promise; + addRoomHasActiveConnections(roomId: string): void; + removeRoomHasActiveConnections(roomId: string): void; + getAllRoomsWithActiveConnections(): Promise; } \ No newline at end of file