Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NODE-3467): implement srvMaxHosts, srvServiceName options #3031

Merged
merged 30 commits into from Nov 16, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ce4d7e4
feat(NODE-3467): implement srvMaxHosts, srvServiceName, and rescanSrv…
nbbeeken Nov 8, 2021
6321246
fix: unit tests
nbbeeken Nov 9, 2021
52b3377
fix: integration tests
nbbeeken Nov 9, 2021
676f8f3
wip
nbbeeken Nov 9, 2021
7879773
test: fix up shuffle unit tests
nbbeeken Nov 9, 2021
1d09e18
fix: shuffle tests round 2 undo srv event saving
nbbeeken Nov 10, 2021
f7c304d
Apply suggestions from code review
nbbeeken Nov 10, 2021
eca1ee4
test: remove dupe test
nbbeeken Nov 10, 2021
471dc61
fix: remove unused equals method
nbbeeken Nov 10, 2021
0b35275
docs: improve limit description
nbbeeken Nov 10, 2021
5daa3ce
fix: lint
nbbeeken Nov 10, 2021
382ad4d
fix: remove rescan option and drop TXT record option logic
nbbeeken Nov 10, 2021
852fca3
fix: permit new options only on srv connection strings
nbbeeken Nov 10, 2021
28567f8
fix: address comments, fix option parsing errors, test naming
nbbeeken Nov 11, 2021
00a39d1
fix: LB connection string assertion
nbbeeken Nov 11, 2021
12a72ab
feat: super algorithm enhancements O(-1) speeds
nbbeeken Nov 11, 2021
5fa6550
fix: shuffle lowerBound logic, test for srvServiceName length error
nbbeeken Nov 12, 2021
537ec45
or -> nor
nbbeeken Nov 12, 2021
0db7bb8
fix: address comments except for connection_string tests
nbbeeken Nov 15, 2021
42a0942
fix: whoops broke host gathering, fixed now
nbbeeken Nov 15, 2021
0db9494
suggestions!
nbbeeken Nov 15, 2021
60e3c75
move tests into correct places, update initial seed list testing WIP
nbbeeken Nov 15, 2021
34bc0d4
add ticket mention
nbbeeken Nov 15, 2021
beb74c5
clarify records
nbbeeken Nov 15, 2021
0e45319
remove comment
nbbeeken Nov 15, 2021
d9e8f28
prevent mutation
nbbeeken Nov 15, 2021
eec372e
test: add object option test, clean up assertions
nbbeeken Nov 16, 2021
7465308
call makeStubs first in test 13
nbbeeken Nov 16, 2021
ad521aa
fix: check for nullish srvMaxHosts
nbbeeken Nov 16, 2021
09b30d6
fix: increase the input size
nbbeeken Nov 16, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .mocharc.json
Expand Up @@ -5,8 +5,8 @@
"ts"
],
"require": [
"ts-node/register",
"source-map-support/register",
"ts-node/register",
"test/tools/runner/chai-addons",
"test/tools/runner/circular-dep-hack"
],
Expand Down
57 changes: 48 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -47,6 +47,7 @@
"@types/node": "^16.10.3",
"@types/saslprep": "^1.0.1",
"@types/semver": "^7.3.8",
"@types/sinon": "^10.0.6",
"@types/whatwg-url": "^8.2.1",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
Expand All @@ -69,7 +70,7 @@
"prettier": "^2.4.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"sinon": "^11.1.2",
"sinon": "^12.0.1",
"sinon-chai": "^3.7.0",
"source-map-support": "^0.5.20",
"standard-version": "^9.3.1",
Expand Down
58 changes: 51 additions & 7 deletions src/connection_string.ts
Expand Up @@ -34,7 +34,7 @@ import { PromiseProvider } from './promise_provider';
import { Encrypter } from './encrypter';
import { Compressor, CompressorName } from './cmap/wire_protocol/compression';

const VALID_TXT_RECORDS = ['authSource', 'replicaSet', 'loadBalanced'];
const VALID_TXT_RECORDS = ['authSource', 'replicaSet', 'loadBalanced', 'srvMaxHosts'];

const LB_SINGLE_HOST_ERROR = 'loadBalanced option only supported with a single host in the URI';
const LB_REPLICA_SET_ERROR = 'loadBalanced option not supported with a replicaSet option';
Expand Down Expand Up @@ -75,7 +75,7 @@ export function resolveSRVRecord(options: MongoOptions, callback: Callback<HostA

// Resolve the SRV record and use the result as the list of hosts to connect to.
const lookupAddress = options.srvHost;
dns.resolveSrv(`_mongodb._tcp.${lookupAddress}`, (err, addresses) => {
dns.resolveSrv(`_${options.srvServiceName}._tcp.${lookupAddress}`, (err, addresses) => {
if (err) return callback(err);

if (addresses.length === 0) {
Expand Down Expand Up @@ -116,12 +116,21 @@ export function resolveSRVRecord(options: MongoOptions, callback: Callback<HostA
);
}

if (VALID_TXT_RECORDS.some(option => txtRecordOptions.get(option) === '')) {
return callback(new MongoParseError('Cannot have empty URI params in DNS TXT Record'));
}

const source = txtRecordOptions.get('authSource') ?? undefined;
const replicaSet = txtRecordOptions.get('replicaSet') ?? undefined;
const loadBalanced = txtRecordOptions.get('loadBalanced') ?? undefined;
const srvMaxHostsString = txtRecordOptions.get('srvMaxHosts') ?? undefined;

if (source === '' || replicaSet === '') {
return callback(new MongoParseError('Cannot have empty URI params in DNS TXT Record'));
if (srvMaxHostsString) {
try {
options.srvMaxHosts = getUint('srvMaxHosts', txtRecordOptions.get('srvMaxHosts'));
} catch (error) {
return callback(error);
}
}

if (!options.userSpecifiedAuthSource && source) {
Expand All @@ -132,6 +141,14 @@ export function resolveSRVRecord(options: MongoOptions, callback: Callback<HostA
options.replicaSet = replicaSet;
}

if (
options.replicaSet &&
typeof options.srvMaxHosts === 'number' &&
dariakp marked this conversation as resolved.
Show resolved Hide resolved
options.srvMaxHosts !== 0
) {
return callback(new MongoParseError('Cannot combine replicaSet option with srvMaxHosts'));
}

if (loadBalanced === 'true') {
options.loadBalanced = true;
}
Expand Down Expand Up @@ -254,9 +271,6 @@ export function parseOptions(
if (isSRV) {
// SRV Record is resolved upon connecting
mongoOptions.srvHost = hosts[0];
if (!url.searchParams.has('tls') && !url.searchParams.has('ssl')) {
options.tls = true;
}
}

const urlOptions = new CaseInsensitiveMap();
Expand Down Expand Up @@ -315,6 +329,16 @@ export function parseOptions(

const allOptions = new CaseInsensitiveMap();

if (isSRV) {
dariakp marked this conversation as resolved.
Show resolved Hide resolved
// SRV turns on TLS by default, but users can override and turn it off
const noUserSpecifiedTLS = !objectOptions.has('tls') && !urlOptions.has('tls');
const noUserSpecifiedSSL = !objectOptions.has('ssl') && !urlOptions.has('ssl');
if (noUserSpecifiedTLS && noUserSpecifiedSSL) {
objectOptions.set('tls', true);
objectOptions.set('ssl', true);
}
}

const allKeys = new Set<string>([
...urlOptions.keys(),
...objectOptions.keys(),
Expand Down Expand Up @@ -366,6 +390,14 @@ export function parseOptions(
setOption(mongoOptions, key, descriptor, values);
}

if (
typeof mongoOptions.srvMaxHosts === 'number' &&
mongoOptions.srvMaxHosts !== 0 &&
typeof mongoOptions.replicaSet === 'string'
) {
throw new MongoParseError('Cannot combine replicaSet option and maxSrvHosts');
}

if (mongoOptions.credentials) {
const isGssapi = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_GSSAPI;
const isX509 = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_X509;
Expand Down Expand Up @@ -439,6 +471,10 @@ function validateLoadBalancedOptions(
if (mongoOptions.directConnection) {
return new MongoParseError(LB_DIRECT_CONNECTION_ERROR);
}

if (mongoOptions.srvHost && mongoOptions.srvMaxHosts) {
return new MongoParseError('Cannot limit srv hosts with loadBalanced enabled');
}
}
}

Expand Down Expand Up @@ -902,6 +938,7 @@ export const OPTIONS = {
replicaSet: {
type: 'string'
},
rescanSrvIntervalMS: { type: 'uint', default: 60000 },
retryReads: {
default: true,
type: 'boolean'
Expand All @@ -924,6 +961,13 @@ export const OPTIONS = {
default: 0,
type: 'uint'
},
srvMaxHosts: {
type: 'uint'
dariakp marked this conversation as resolved.
Show resolved Hide resolved
},
srvServiceName: {
type: 'string',
default: 'mongodb'
dariakp marked this conversation as resolved.
Show resolved Hide resolved
},
ssl: {
target: 'tls',
type: 'boolean'
Expand Down
15 changes: 15 additions & 0 deletions src/mongo_client.ts
Expand Up @@ -132,6 +132,18 @@ export interface MongoClientOptions extends BSONSerializeOptions, SupportedNodeC
compressors?: CompressorName[] | string;
/** An integer that specifies the compression level if using zlib for network compression. */
zlibCompressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | undefined;
/** The maximum number of hosts to connect to when using an srv connection string */
dariakp marked this conversation as resolved.
Show resolved Hide resolved
srvMaxHosts?: number;
/**
* Modifies the srv URI to look like:
*
* `_{srvServiceName}._tcp.{hostname}.{domainname}`
*
* Querying this DNS URI is expected to respond with SRV records
*/
srvServiceName?: string;
/** Frequency with which to scan SRV record changes */
rescanSrvIntervalMS?: number;
dariakp marked this conversation as resolved.
Show resolved Hide resolved
/** The maximum number of connections in the connection pool. */
maxPoolSize?: number;
/** The minimum number of connections in the connection pool. */
Expand Down Expand Up @@ -643,9 +655,12 @@ export interface MongoOptions
| 'retryWrites'
| 'serverSelectionTimeoutMS'
| 'socketTimeoutMS'
| 'srvMaxHosts'
| 'srvServiceName'
| 'tlsAllowInvalidCertificates'
| 'tlsAllowInvalidHostnames'
| 'tlsInsecure'
| 'rescanSrvIntervalMS'
| 'waitQueueTimeoutMS'
| 'zlibCompressionLevel'
>
Expand Down
11 changes: 9 additions & 2 deletions src/operations/connect.ts
@@ -1,7 +1,8 @@
import type * as dns from 'dns';
import { MongoRuntimeError, MongoInvalidArgumentError } from '../error';
import { Topology, TOPOLOGY_EVENTS } from '../sdam/topology';
import { resolveSRVRecord } from '../connection_string';
import type { Callback } from '../utils';
import { Callback, shuffle } from '../utils';
import type { MongoClient, MongoOptions } from '../mongo_client';
import { CMAP_EVENTS } from '../cmap/connection_pool';
import { APM_EVENTS } from '../cmap/connection';
Expand Down Expand Up @@ -51,7 +52,13 @@ export function connect(
if (typeof options.srvHost === 'string') {
return resolveSRVRecord(options, (err, hosts) => {
if (err || !hosts) return callback(err);
for (const [index, host] of hosts.entries()) {

const selectedHosts =
options.srvMaxHosts === 0 || options.srvMaxHosts >= hosts.length
? hosts
: shuffle(hosts, options.srvMaxHosts);

for (const [index, host] of selectedHosts.entries()) {
options.hosts[index] = host;
}

Expand Down