Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions packages/cli-repl/test/e2e-direct.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,16 @@ describe('e2e direct connection', () => {
});
});
});

describe('fail-fast connections', () => {
it('does not fail fast for ECONNREFUSED errors when one host is reachable', async() => {
const shell = TestShell.start({ args: [
`mongodb://${await rs1.hostport()},127.0.0.1:1/?replicaSet=${replSetId}&readPreference=secondary`
] });
const result = await shell.waitForPromptOrExit();
expect(result).to.deep.equal({ state: 'prompt' });
});
});
});
});
});
18 changes: 16 additions & 2 deletions packages/cli-repl/test/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -939,9 +939,23 @@ describe('e2e', function() {
expect(exitCode).to.equal(1);
});

it('fails fast for ECONNREFUSED errors', async() => {
it('fails fast for ECONNREFUSED errors to a single host', async() => {
const shell = TestShell.start({ args: [
'--port', '0'
'--port', '1'
] });
const result = await shell.waitForPromptOrExit();
expect(result).to.deep.equal({ state: 'exit', exitCode: 1 });
});

it('fails fast for ECONNREFUSED errors to multiple hosts', async function() {
if (process.platform === 'darwin') {
// On macOS, for some reason only connection that fails is the 127.0.0.1:1
// one, over and over. It should be fine to only skip the test there, as this
// isn't a shell-specific issue.
return this.skip();
}
const shell = TestShell.start({ args: [
'mongodb://127.0.0.1:1,127.0.0.2:1,127.0.0.3:1/?replicaSet=foo&readPreference=secondary'
] });
const result = await shell.waitForPromptOrExit();
expect(result).to.deep.equal({ state: 'exit', exitCode: 1 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ describe('CliServiceProvider', () => {
connect() {}
db() {}
close() {}
topology: any;
}

it('connects once when no AutoEncryption set', async() => {
Expand Down Expand Up @@ -141,9 +142,16 @@ describe('CliServiceProvider', () => {
mClient.connect = () => new Promise((resolve, reject) => {
rejectConnect = reject;
setImmediate(() => {
mClient.emit('serverHeartbeatFailed', { failure: err });
mClient.emit('serverHeartbeatFailed', { failure: err, connectionId: uri });
});
});
mClient.topology = {
description: {
servers: new Map([
['localhost:27017', {}]
])
}
};
try {
await connectMongoClient(uri, {}, mClientType as any);
} catch (e) {
Expand Down
34 changes: 28 additions & 6 deletions packages/service-provider-server/src/cli-service-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import {
ReadPreferenceFromOptions,
ReadPreferenceLike,
OperationOptions,
ServerHeartbeatFailedEvent
ServerHeartbeatFailedEvent,
ServerHeartbeatSucceededEvent,
TopologyDescription
} from 'mongodb';

import {
Expand Down Expand Up @@ -141,21 +143,41 @@ const DEFAULT_BASE_OPTIONS: OperationOptions = Object.freeze({
* errors.
*/
async function connectWithFailFast(client: MongoClient): Promise<void> {
let failFastErr;
const heartbeatFailureListener = ({ failure }: ServerHeartbeatFailedEvent) => {
const failedConnections = new Map<string, Error>();
let failedEarly = false;

const heartbeatFailureListener = ({ failure, connectionId }: ServerHeartbeatFailedEvent) => {
const topologyDescription: TopologyDescription | undefined = (client as any).topology?.description;
const servers = topologyDescription?.servers;
if (!servers?.has(connectionId)) {
return; // TODO: Make a way to log this condition to the mongosh bus.
}

if (isFastFailureConnectionError(failure)) {
failFastErr = failure;
client.close();
failedConnections.set(connectionId, failure);
if ([...servers.keys()].every(server => failedConnections.has(server))) {
failedEarly = true;
client.close();
}
}
};

const heartbeatSucceededListener = ({ connectionId }: ServerHeartbeatSucceededEvent) => {
failedConnections.delete(connectionId);
};

client.addListener('serverHeartbeatFailed', heartbeatFailureListener);
client.addListener('serverHeartbeatSucceeded', heartbeatSucceededListener);
try {
await client.connect();
} catch (err) {
throw failFastErr || err;
if (failedEarly) {
throw failedConnections.values().next().value; // Just use the first failure.
}
throw err;
} finally {
client.removeListener('serverHeartbeatFailed', heartbeatFailureListener);
client.removeListener('serverHeartbeatSucceeded', heartbeatSucceededListener);
}
}

Expand Down