Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 62 additions & 46 deletions packages/compass-app-stores/src/stores/instance-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<ReturnType<DataService['instance']>>;

function serversArray(
serversMap: NonNullable<
ReturnType<DataService['getLastSeenTopology']>
Expand Down Expand Up @@ -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<MongoDBInstanceProps> = {
_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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're now tacking on more positional arguments. Wouldn't one object where you can just pick what you want from it start to make more sense? Or do we have too many things using this and that would involve lots of changes? Or is there some other reason I can't immediately spot why we can't change it?

Copy link
Collaborator Author

@gribnoysup gribnoysup Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a closer look at the usage, adding a positional was just the easiest for me, but generally speaking yes, we can probably pack it all into one object (also instanceConnectionId is just connectionInfo.id, so I'll just drop it I think)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went back and forth on this and decided not to change this, I started moving them to an object of properties, but then disconnect event is different and wanted to keep them consistent, but then connectionId is like the only used argument in disconnect and having it inside an object was weird, so I was trying different combinations, like maybe keeping connectionId as an argument, but moving everything else into an object in a second argument, and this also didn't look right, then I thought I'm overthinking it and it was cleaner to just keep it as is. Hope that's okay

_connectionInfo: ConnectionInfo,
instanceInfo: InstanceDetails
) {
const dataService =
connections.getDataServiceForConnection(instanceConnectionId);
const connectionString = dataService.getConnectionString();
const firstHost = connectionString.hosts[0] || '';
const [hostname, port] = firstHost.split(':');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not new code, but isn't this problematic for IPv6 addresses

Copy link
Collaborator Author

@gribnoysup gribnoysup Jul 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good catch (and we are accounting for it in other places), I'll adjust while I'm moving stuff around here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in a8f551c


const initialInstanceProps: Partial<MongoDBInstanceProps> = {
_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<MongoDBInstanceProps>),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm reading this wrong, but don't you want status and statusError to override what's already on instanceInfo rather than the other way around?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I can just pass it all one line higher to the constructor. Generally speaking you're right about this, on practice we won't get those as part of instanceInfo, but absolutely doesn't hurt to be more precise about this

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in a8f551c

});

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<DataService['getLastSeenTopology']>;
}) => {
instance.set({
topologyDescription: getTopologyDescription(newDescription),
});
}
);
});
on(
dataService,
'topologyDescriptionChanged',
({
newDescription,
}: {
newDescription: ReturnType<DataService['getLastSeenTopology']>;
}) => {
instance.set({
topologyDescription: getTopologyDescription(newDescription),
});
}
);
}
);

on(
globalAppRegistry,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
ConnectionAttempt,
ConnectionOptions,
DataService,
InstanceDetails,
} from 'mongodb-data-service';
import { createConnectionAttempt } from 'mongodb-data-service';
import { UUID } from 'bson';
Expand Down Expand Up @@ -43,7 +44,8 @@ import type { ImportConnectionOptions } from '@mongodb-js/connection-storage/pro
export type ConnectionsEventMap = {
connected: (
connectionId: ConnectionId,
connectionInfo: ConnectionInfo
connectionInfo: ConnectionInfo,
instanceInfo: InstanceDetails
) => void;
disconnected: (
connectionId: ConnectionId,
Expand Down Expand Up @@ -1658,6 +1660,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
Comment on lines +1663 to +1665
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, in fact it doesn't really matter what we set our pool size to, browsers do not permit more than 6 parallel connections to be creating at the same time.

It obviously matters for our total count in the long run, but at least at boot that's our bottleneck.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still have multiplexing in the plans, right? Just wondering

// 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();
Copy link
Collaborator Author

@gribnoysup gribnoysup Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is somewhat a breaking change in Compass behavior as described in COMPASS-7675 but I think it's probably okay to do so as we were already discussing doing this. Basically the main change in the behavior here is that some connection related errors that would show up in this weird state where you connected, but we didn't even manage to get to the listing database part of the logic before now will start showing up as part of the connection flow, this doesn't sound too bad to me, but I also asked Betsy to confirm in the ticket


let showedNonRetryableErrorToast = false;
// Listen for non-retry-able errors on failed server heartbeats.
// These can happen on compass web when:
Expand Down Expand Up @@ -1766,7 +1776,7 @@ const connectWithOptions = (
{ dataLake, genuineMongoDB, host, build, isAtlas, isLocalAtlas },
[extraInfo, resolvedHostname],
] = await Promise.all([
dataService.instance(),
instanceInfo,
getExtraConnectionData(connectionInfo),
]);

Expand Down Expand Up @@ -1811,44 +1821,29 @@ const connectWithOptions = (
connectionsEventEmitter.emit(
'connected',
connectionInfo.id,
connectionInfo
connectionInfo,
instanceInfo
);

dispatch({
type: ActionTypes.ConnectionAttemptSuccess,
connectionId: connectionInfo.id,
});

const { networkTraffic, showEndOfLifeConnectionModal } =
preferences.getPreferences();
const { showEndOfLifeConnectionModal } = preferences.getPreferences();

if (
getGenuineMongoDB(connectionInfo.connectionOptions.connectionString)
.isGenuine === false
) {
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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So showEndOfLifeMongoDBWarningModal() now may or may not show the modal because it looks up latestEndOfLifeServerVersion and decides for itself?

Wondering if it should be renamed to something like maybeShowEndOfLifeMongoDBWarningModal() to reflect that. (yes I realise that that's a wildly long and specific name as I type it..)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't mind renaming it. Also maybe I should've kept it under an if statement here to be honest, I'll take another look and adjust either way

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to split into two methods, updated in 7ecdc82

showEndOfLifeMongoDBWarningModal(
connectionInfo.id,
instanceInfo.build.version
)
);
}
} catch (err) {
dispatch(connectionAttemptError(connectionInfo, err));
Expand Down Expand Up @@ -2176,11 +2171,31 @@ export const showNonGenuineMongoDBWarningModal = (
export const showEndOfLifeMongoDBWarningModal = (
connectionId: string,
version: string
): ConnectionsThunkAction<void> => {
return (_dispatch, getState, { track }) => {
const connectionInfo = getCurrentConnectionInfo(getState(), connectionId);
track('Screen', { name: 'end_of_life_mongodb_modal' }, connectionInfo);
void _showEndOfLifeMongoDBWarningModal(connectionInfo, version);
): ConnectionsThunkAction<Promise<void>> => {
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
);
}
};
};

Expand Down
8 changes: 8 additions & 0 deletions packages/data-service/src/data-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ export interface DataService {

/**
* Get the current instance details.
*
* @deprecated avoid using `instance` directly and use `InstanceModel` instead
Copy link
Collaborator Author

@gribnoysup gribnoysup Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Literally everywhere except for those initial / inside models fetches we should be using models instead, so I'm adding this marking to make IDEs highlight the method usage

*/
instance(): Promise<InstanceDetails>;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
Loading