From 626c9997fc360cbe86135551d6d479c99004d036 Mon Sep 17 00:00:00 2001 From: Sergey Petushkov Date: Thu, 3 Jul 2025 17:08:10 +0200 Subject: [PATCH 1/4] chore(data-service, connections): make sure that instance info is only fetched once on initial connection --- .../src/stores/instance-store.ts | 108 ++++++++++-------- .../src/stores/connections-store-redux.ts | 77 ++++++++----- packages/data-service/src/data-service.ts | 8 ++ 3 files changed, 116 insertions(+), 77 deletions(-) diff --git a/packages/compass-app-stores/src/stores/instance-store.ts b/packages/compass-app-stores/src/stores/instance-store.ts index 27fa52958a9..d5edbbedefc 100644 --- a/packages/compass-app-stores/src/stores/instance-store.ts +++ b/packages/compass-app-stores/src/stores/instance-store.ts @@ -2,6 +2,7 @@ import type { MongoDBInstanceProps } from 'mongodb-instance-model'; import { MongoDBInstance } from 'mongodb-instance-model'; import toNS from 'mongodb-ns'; import type { + ConnectionInfo, ConnectionsService, DataService, } from '@mongodb-js/compass-connections/provider'; @@ -14,6 +15,8 @@ import { openToast } from '@mongodb-js/compass-components'; import { MongoDBInstancesManager } from '../instances-manager'; import type { PreferencesAccess } from 'compass-preferences-model'; +type InstanceDetails = Awaited>; + function serversArray( serversMap: NonNullable< ReturnType @@ -305,55 +308,68 @@ export function createInstancesStore( instancesManager.removeMongoDBInstanceForConnection(connectionInfoId); }); - on(connections, 'connected', function (instanceConnectionId: string) { - const dataService = - connections.getDataServiceForConnection(instanceConnectionId); - const connectionString = dataService.getConnectionString(); - const firstHost = connectionString.hosts[0] || ''; - const [hostname, port] = firstHost.split(':'); - - const initialInstanceProps: Partial = { - _id: firstHost, - hostname: hostname, - port: port ? +port : undefined, - topologyDescription: getTopologyDescription( - dataService.getLastSeenTopology() - ), - preferences, - }; - const instance = instancesManager.createMongoDBInstanceForConnection( - instanceConnectionId, - initialInstanceProps as MongoDBInstanceProps - ); + on( + connections, + 'connected', + function ( + instanceConnectionId: string, + _connectionInfo: ConnectionInfo, + instanceInfo: InstanceDetails + ) { + const dataService = + connections.getDataServiceForConnection(instanceConnectionId); + const connectionString = dataService.getConnectionString(); + const firstHost = connectionString.hosts[0] || ''; + const [hostname, port] = firstHost.split(':'); + + const initialInstanceProps: Partial = { + _id: firstHost, + hostname: hostname, + port: port ? +port : undefined, + topologyDescription: getTopologyDescription( + dataService.getLastSeenTopology() + ), + preferences, + }; + const instance = instancesManager.createMongoDBInstanceForConnection( + instanceConnectionId, + initialInstanceProps as MongoDBInstanceProps + ); + instance.set({ + status: 'ready', + statusError: null, + ...(instanceInfo as Partial), + }); - addCleanup(() => { - instance.removeAllListeners(); - }); + addCleanup(() => { + instance.removeAllListeners(); + }); - void refreshInstance( - { - fetchDatabases: true, - fetchDbStats: true, - }, - { - connectionId: instanceConnectionId, - } - ); + void refreshInstance( + { + fetchDatabases: true, + fetchDbStats: true, + }, + { + connectionId: instanceConnectionId, + } + ); - on( - dataService, - 'topologyDescriptionChanged', - ({ - newDescription, - }: { - newDescription: ReturnType; - }) => { - instance.set({ - topologyDescription: getTopologyDescription(newDescription), - }); - } - ); - }); + on( + dataService, + 'topologyDescriptionChanged', + ({ + newDescription, + }: { + newDescription: ReturnType; + }) => { + instance.set({ + topologyDescription: getTopologyDescription(newDescription), + }); + } + ); + } + ); on( globalAppRegistry, diff --git a/packages/compass-connections/src/stores/connections-store-redux.ts b/packages/compass-connections/src/stores/connections-store-redux.ts index 55eabbc396d..b32c9c615eb 100644 --- a/packages/compass-connections/src/stores/connections-store-redux.ts +++ b/packages/compass-connections/src/stores/connections-store-redux.ts @@ -16,6 +16,7 @@ import type { ConnectionAttempt, ConnectionOptions, DataService, + InstanceDetails, } from 'mongodb-data-service'; import { createConnectionAttempt } from 'mongodb-data-service'; import { UUID } from 'bson'; @@ -42,7 +43,8 @@ import { export type ConnectionsEventMap = { connected: ( connectionId: ConnectionId, - connectionInfo: ConnectionInfo + connectionInfo: ConnectionInfo, + instanceInfo: InstanceDetails ) => void; disconnected: ( connectionId: ConnectionId, @@ -1657,6 +1659,14 @@ const connectWithOptions = ( return; } + // We're trying to optimise the initial Compass loading times here: to + // make sure that the driver connection pool doesn't immediately get + // overwhelmed with requests, we fetch instance info only once and then + // pass it down to telemetry and instance model. This is a relatively + // expensive dataService operation so we're trying to keep the usage + // very limited + const instanceInfo = await dataService.instance(); + let showedNonRetryableErrorToast = false; // Listen for non-retry-able errors on failed server heartbeats. // These can happen on compass web when: @@ -1765,7 +1775,7 @@ const connectWithOptions = ( { dataLake, genuineMongoDB, host, build, isAtlas, isLocalAtlas }, [extraInfo, resolvedHostname], ] = await Promise.all([ - dataService.instance(), + instanceInfo, getExtraConnectionData(connectionInfo), ]); @@ -1810,7 +1820,8 @@ const connectWithOptions = ( connectionsEventEmitter.emit( 'connected', connectionInfo.id, - connectionInfo + connectionInfo, + instanceInfo ); dispatch({ @@ -1818,8 +1829,7 @@ const connectWithOptions = ( connectionId: connectionInfo.id, }); - const { networkTraffic, showEndOfLifeConnectionModal } = - preferences.getPreferences(); + const { showEndOfLifeConnectionModal } = preferences.getPreferences(); if ( getGenuineMongoDB(connectionInfo.connectionOptions.connectionString) @@ -1827,27 +1837,12 @@ const connectWithOptions = ( ) { dispatch(showNonGenuineMongoDBWarningModal(connectionInfo.id)); } else if (showEndOfLifeConnectionModal) { - void dataService - .instance() - .then(async (instance) => { - const { version } = instance.build; - const latestEndOfLifeServerVersion = - await getLatestEndOfLifeServerVersion(networkTraffic); - if (isEndOfLifeVersion(version, latestEndOfLifeServerVersion)) { - dispatch( - showEndOfLifeMongoDBWarningModal( - connectionInfo.id, - instance.build.version - ) - ); - } - }) - .catch((err) => { - debug( - 'failed to get instance details to determine if the server version is end-of-life', - err - ); - }); + void dispatch( + showEndOfLifeMongoDBWarningModal( + connectionInfo.id, + instanceInfo.build.version + ) + ); } } catch (err) { dispatch(connectionAttemptError(connectionInfo, err)); @@ -2175,11 +2170,31 @@ export const showNonGenuineMongoDBWarningModal = ( export const showEndOfLifeMongoDBWarningModal = ( connectionId: string, version: string -): ConnectionsThunkAction => { - return (_dispatch, getState, { track }) => { - const connectionInfo = getCurrentConnectionInfo(getState(), connectionId); - track('Screen', { name: 'end_of_life_mongodb_modal' }, connectionInfo); - void _showEndOfLifeMongoDBWarningModal(connectionInfo, version); +): ConnectionsThunkAction> => { + return async ( + _dispatch, + getState, + { track, logger: { debug }, preferences } + ) => { + try { + const latestEndOfLifeServerVersion = + await getLatestEndOfLifeServerVersion( + preferences.getPreferences().networkTraffic + ); + if (isEndOfLifeVersion(version, latestEndOfLifeServerVersion)) { + const connectionInfo = getCurrentConnectionInfo( + getState(), + connectionId + ); + track('Screen', { name: 'end_of_life_mongodb_modal' }, connectionInfo); + void _showEndOfLifeMongoDBWarningModal(connectionInfo, version); + } + } catch (err) { + debug( + 'failed to get instance details to determine if the server version is end-of-life', + err + ); + } }; }; diff --git a/packages/data-service/src/data-service.ts b/packages/data-service/src/data-service.ts index 17bb28287bb..83ff79cf008 100644 --- a/packages/data-service/src/data-service.ts +++ b/packages/data-service/src/data-service.ts @@ -310,6 +310,8 @@ export interface DataService { /** * Get the current instance details. + * + * @deprecated avoid using `instance` directly and use `InstanceModel` instead */ instance(): Promise; @@ -340,6 +342,9 @@ export interface DataService { /** * List all collections for a database. + * + * @deprecated avoid using `listCollections` directly and use + * `CollectionModel` instead */ listCollections( databaseName: string, @@ -448,6 +453,9 @@ export interface DataService { /** * List all databases on the currently connected instance. + * + * @deprecated avoid using `listDatabases` directly and use `DatabaseModel` + * instead */ listDatabases(options?: { nameOnly?: true; From 7ecdc82a8cf39ef62a46c9a880d91ff0c9d9d314 Mon Sep 17 00:00:00 2001 From: Sergey Petushkov Date: Mon, 14 Jul 2025 18:43:41 +0200 Subject: [PATCH 2/4] chore(connections): separate should show and show methods --- .../src/stores/connections-store-redux.ts | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/packages/compass-connections/src/stores/connections-store-redux.ts b/packages/compass-connections/src/stores/connections-store-redux.ts index d4b31b99528..7f504bc58b9 100644 --- a/packages/compass-connections/src/stores/connections-store-redux.ts +++ b/packages/compass-connections/src/stores/connections-store-redux.ts @@ -1830,15 +1830,19 @@ const connectWithOptions = ( connectionId: connectionInfo.id, }); - const { showEndOfLifeConnectionModal } = preferences.getPreferences(); - if ( getGenuineMongoDB(connectionInfo.connectionOptions.connectionString) .isGenuine === false ) { dispatch(showNonGenuineMongoDBWarningModal(connectionInfo.id)); - } else if (showEndOfLifeConnectionModal) { - void dispatch( + } else if ( + await shouldShowEndOfLifeWarning( + instanceInfo.build.version, + preferences, + debug + ) + ) { + dispatch( showEndOfLifeMongoDBWarningModal( connectionInfo.id, instanceInfo.build.version @@ -2168,34 +2172,38 @@ export const showNonGenuineMongoDBWarningModal = ( }; }; +async function shouldShowEndOfLifeWarning( + serverVersion: string, + preferences: PreferencesAccess, + debug: Logger['debug'] +) { + try { + const { showEndOfLifeConnectionModal, networkTraffic } = + preferences.getPreferences(); + if (!showEndOfLifeConnectionModal) { + return; + } + const latestEndOfLifeServerVersion = await getLatestEndOfLifeServerVersion( + networkTraffic + ); + return isEndOfLifeVersion(serverVersion, latestEndOfLifeServerVersion); + } catch (err) { + debug( + 'failed to get instance details to determine if the server version is end-of-life', + err + ); + return false; + } +} + export const showEndOfLifeMongoDBWarningModal = ( connectionId: string, version: string -): ConnectionsThunkAction> => { - return async ( - _dispatch, - getState, - { track, logger: { debug }, preferences } - ) => { - try { - const latestEndOfLifeServerVersion = - await getLatestEndOfLifeServerVersion( - preferences.getPreferences().networkTraffic - ); - if (isEndOfLifeVersion(version, latestEndOfLifeServerVersion)) { - const connectionInfo = getCurrentConnectionInfo( - getState(), - connectionId - ); - track('Screen', { name: 'end_of_life_mongodb_modal' }, connectionInfo); - void _showEndOfLifeMongoDBWarningModal(connectionInfo, version); - } - } catch (err) { - debug( - 'failed to get instance details to determine if the server version is end-of-life', - err - ); - } +): ConnectionsThunkAction => { + return (_dispatch, getState, { track }) => { + const connectionInfo = getCurrentConnectionInfo(getState(), connectionId); + track('Screen', { name: 'end_of_life_mongodb_modal' }, connectionInfo); + void _showEndOfLifeMongoDBWarningModal(connectionInfo, version); }; }; From a8f551c0fb7d3971e0b9d77234788d1bdd6a862a Mon Sep 17 00:00:00 2001 From: Sergey Petushkov Date: Mon, 14 Jul 2025 18:52:22 +0200 Subject: [PATCH 3/4] chore(app-stores): handle ipv6 hosts when building data for instance model; pass instance info to constructor instead of extra .set call --- .../src/stores/instance-store.ts | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/compass-app-stores/src/stores/instance-store.ts b/packages/compass-app-stores/src/stores/instance-store.ts index d5edbbedefc..281ec5786a8 100644 --- a/packages/compass-app-stores/src/stores/instance-store.ts +++ b/packages/compass-app-stores/src/stores/instance-store.ts @@ -320,26 +320,36 @@ export function createInstancesStore( connections.getDataServiceForConnection(instanceConnectionId); const connectionString = dataService.getConnectionString(); const firstHost = connectionString.hosts[0] || ''; - const [hostname, port] = firstHost.split(':'); + const [hostname, port] = (() => { + if (firstHost.startsWith('[')) { + return firstHost.slice(1).split(']'); // IPv6 + } + return firstHost.split(':'); + })(); const initialInstanceProps: Partial = { + // We pre-fetched instance info and so can right away construct it in a + // "ready" state + ...(instanceInfo as Partial), + status: 'ready', + statusError: null, + + // Required initial values that are not returned with instance info _id: firstHost, hostname: hostname, port: port ? +port : undefined, topologyDescription: getTopologyDescription( dataService.getLastSeenTopology() ), + + // Service injection for preferences (currently only controls namespace + // stats fetching) preferences, }; const instance = instancesManager.createMongoDBInstanceForConnection( instanceConnectionId, initialInstanceProps as MongoDBInstanceProps ); - instance.set({ - status: 'ready', - statusError: null, - ...(instanceInfo as Partial), - }); addCleanup(() => { instance.removeAllListeners(); From 980508fa2a0a745c4140732986bf11d30bd71384 Mon Sep 17 00:00:00 2001 From: Sergey Petushkov Date: Mon, 14 Jul 2025 19:07:58 +0200 Subject: [PATCH 4/4] chore(connections): no need for promise.all --- .../src/stores/connections-store-redux.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/compass-connections/src/stores/connections-store-redux.ts b/packages/compass-connections/src/stores/connections-store-redux.ts index 7f504bc58b9..700cdf17e32 100644 --- a/packages/compass-connections/src/stores/connections-store-redux.ts +++ b/packages/compass-connections/src/stores/connections-store-redux.ts @@ -1772,13 +1772,17 @@ const connectWithOptions = ( track( 'New Connection', async () => { - const [ - { dataLake, genuineMongoDB, host, build, isAtlas, isLocalAtlas }, - [extraInfo, resolvedHostname], - ] = await Promise.all([ - instanceInfo, - getExtraConnectionData(connectionInfo), - ]); + const { + dataLake, + genuineMongoDB, + host, + build, + isAtlas, + isLocalAtlas, + } = instanceInfo; + const [extraInfo, resolvedHostname] = await getExtraConnectionData( + connectionInfo + ); const connections = getState().connections; // Counting all connections, we need to filter out any connections currently being created