mongodb
Version:
The official MongoDB driver for Node.js
416 lines (365 loc) • 14.5 kB
text/typescript
import { MongoInvalidArgumentError, MongoRuntimeError } from '../error';
import { ReadPreference } from '../read_preference';
import { ServerType, TopologyType } from './common';
import type { ServerDescription, TagSet } from './server_description';
import type { TopologyDescription } from './topology_description';
// max staleness constants
const IDLE_WRITE_PERIOD = 10000;
const SMALLEST_MAX_STALENESS_SECONDS = 90;
// Minimum version to try writes on secondaries.
export const MIN_SECONDARY_WRITE_WIRE_VERSION = 13;
/** @internal */
export type ServerSelector = (
topologyDescription: TopologyDescription,
servers: ServerDescription[],
deprioritized: DeprioritizedServers
) => ServerDescription[];
/** @internal */
export class DeprioritizedServers {
private deprioritized: Set<string> = new Set();
constructor(descriptions?: Iterable<ServerDescription>) {
for (const description of descriptions ?? []) {
this.add(description);
}
}
add({ address }: ServerDescription) {
this.deprioritized.add(address);
}
has({ address }: ServerDescription): boolean {
return this.deprioritized.has(address);
}
}
function filterDeprioritized(
candidates: ServerDescription[],
deprioritized: DeprioritizedServers
): ServerDescription[] {
const filtered = candidates.filter(candidate => !deprioritized.has(candidate));
return filtered.length ? filtered : candidates;
}
/**
* Returns a server selector that selects for writable servers
*/
export function writableServerSelector(): ServerSelector {
return function writableServer(
topologyDescription: TopologyDescription,
servers: ServerDescription[],
deprioritized: DeprioritizedServers
): ServerDescription[] {
const eligibleServers = filterDeprioritized(
servers.filter(({ isWritable }) => isWritable),
deprioritized
);
return latencyWindowReducer(topologyDescription, eligibleServers);
};
}
/**
* The purpose of this selector is to select the same server, only
* if it is in a state that it can have commands sent to it.
*/
export function sameServerSelector(description?: ServerDescription): ServerSelector {
return function sameServerSelector(
_topologyDescription: TopologyDescription,
servers: ServerDescription[],
_deprioritized: DeprioritizedServers
): ServerDescription[] {
if (!description) return [];
// Filter the servers to match the provided description only if
// the type is not unknown.
return servers.filter(sd => {
return sd.address === description.address && sd.type !== ServerType.Unknown;
});
};
}
/**
* Returns a server selector that uses a read preference to select a
* server potentially for a write on a secondary.
*/
export function secondaryWritableServerSelector(
wireVersion: number,
readPreference?: ReadPreference
): ServerSelector {
// If server version < 5.0, read preference always primary.
// If server version >= 5.0...
// - If read preference is supplied, use that.
// - If no read preference is supplied, use primary.
if (
!readPreference ||
!wireVersion ||
(wireVersion && wireVersion < MIN_SECONDARY_WRITE_WIRE_VERSION)
) {
return readPreferenceServerSelector(ReadPreference.primary);
}
return readPreferenceServerSelector(readPreference);
}
/**
* Reduces the passed in array of servers by the rules of the "Max Staleness" specification
* found here:
*
* @see https://github.com/mongodb/specifications/blob/master/source/max-staleness/max-staleness.md
*
* @param readPreference - The read preference providing max staleness guidance
* @param topologyDescription - The topology description
* @param servers - The list of server descriptions to be reduced
* @returns The list of servers that satisfy the requirements of max staleness
*/
function maxStalenessReducer(
readPreference: ReadPreference,
topologyDescription: TopologyDescription,
servers: ServerDescription[]
): ServerDescription[] {
if (readPreference.maxStalenessSeconds == null || readPreference.maxStalenessSeconds < 0) {
return servers;
}
const maxStaleness = readPreference.maxStalenessSeconds;
const maxStalenessVariance =
(topologyDescription.heartbeatFrequencyMS + IDLE_WRITE_PERIOD) / 1000;
if (maxStaleness < maxStalenessVariance) {
throw new MongoInvalidArgumentError(
`Option "maxStalenessSeconds" must be at least ${maxStalenessVariance} seconds`
);
}
if (maxStaleness < SMALLEST_MAX_STALENESS_SECONDS) {
throw new MongoInvalidArgumentError(
`Option "maxStalenessSeconds" must be at least ${SMALLEST_MAX_STALENESS_SECONDS} seconds`
);
}
if (topologyDescription.type === TopologyType.ReplicaSetWithPrimary) {
const primary: ServerDescription = Array.from(topologyDescription.servers.values()).filter(
primaryFilter
)[0];
return servers.filter((server: ServerDescription) => {
const stalenessMS =
server.lastUpdateTime -
server.lastWriteDate -
(primary.lastUpdateTime - primary.lastWriteDate) +
topologyDescription.heartbeatFrequencyMS;
const staleness = stalenessMS / 1000;
const maxStalenessSeconds = readPreference.maxStalenessSeconds ?? 0;
return staleness <= maxStalenessSeconds;
});
}
if (topologyDescription.type === TopologyType.ReplicaSetNoPrimary) {
if (servers.length === 0) {
return servers;
}
const sMax = servers.reduce((max: ServerDescription, s: ServerDescription) =>
s.lastWriteDate > max.lastWriteDate ? s : max
);
return servers.filter((server: ServerDescription) => {
const stalenessMS =
sMax.lastWriteDate - server.lastWriteDate + topologyDescription.heartbeatFrequencyMS;
const staleness = stalenessMS / 1000;
const maxStalenessSeconds = readPreference.maxStalenessSeconds ?? 0;
return staleness <= maxStalenessSeconds;
});
}
return servers;
}
/**
* Determines whether a server's tags match a given set of tags.
*
* A tagset matches the server's tags if every k-v pair in the tagset
* is also in the server's tagset.
*
* Note that this does not requires that every k-v pair in the server's tagset is also
* in the client's tagset. The server's tagset is required only to be a superset of the
* client's tags.
*
* @see https://github.com/mongodb/specifications/blob/master/source/server-selection/server-selection.md#tag_sets
*
* @param tagSet - The requested tag set to match
* @param serverTags - The server's tags
*/
function tagSetMatch(tagSet: TagSet, serverTags: TagSet) {
return Object.entries(tagSet).every(
([key, value]) => serverTags[key] != null && serverTags[key] === value
);
}
/**
* Reduces a set of server descriptions based on tags requested by the read preference
*
* @param readPreference - The read preference providing the requested tags
* @param servers - The list of server descriptions to reduce
* @returns The list of servers matching the requested tags
*/
function tagSetReducer(
{ tags }: ReadPreference,
servers: ServerDescription[]
): ServerDescription[] {
if (tags == null || tags.length === 0) {
// empty tag sets match all servers
return servers;
}
for (const tagSet of tags) {
const serversMatchingTagset = servers.filter((s: ServerDescription) =>
tagSetMatch(tagSet, s.tags)
);
if (serversMatchingTagset.length) {
return serversMatchingTagset;
}
}
return [];
}
/**
* Reduces a list of servers to ensure they fall within an acceptable latency window. This is
* further specified in the "Server Selection" specification, found here:
*
* @see https://github.com/mongodb/specifications/blob/master/source/server-selection/server-selection.md
*
* @param topologyDescription - The topology description
* @param servers - The list of servers to reduce
* @returns The servers which fall within an acceptable latency window
*/
function latencyWindowReducer(
topologyDescription: TopologyDescription,
servers: ServerDescription[]
): ServerDescription[] {
const low = servers.reduce(
(min: number, server: ServerDescription) => Math.min(server.roundTripTime, min),
Infinity
);
const high = low + topologyDescription.localThresholdMS;
return servers.filter(server => server.roundTripTime <= high && server.roundTripTime >= low);
}
// filters
function primaryFilter(server: ServerDescription): boolean {
return server.type === ServerType.RSPrimary;
}
function secondaryFilter(server: ServerDescription): boolean {
return server.type === ServerType.RSSecondary;
}
function nearestFilter(server: ServerDescription): boolean {
return server.type === ServerType.RSSecondary || server.type === ServerType.RSPrimary;
}
function knownFilter(server: ServerDescription): boolean {
return server.type !== ServerType.Unknown;
}
function loadBalancerFilter(server: ServerDescription): boolean {
return server.type === ServerType.LoadBalancer;
}
function isDeprioritizedFactory(
deprioritized: DeprioritizedServers
): (server: ServerDescription) => boolean {
return server =>
// if any deprioritized servers equal the server, here we are.
!deprioritized.has(server);
}
function secondarySelector(
readPreference: ReadPreference,
topologyDescription: TopologyDescription,
servers: ServerDescription[],
deprioritized: DeprioritizedServers
) {
const mode = readPreference.mode;
switch (mode) {
case 'primary':
// Note: no need to filter for deprioritized servers. A replica set has only one primary; that means that
// we are in one of two scenarios:
// 1. deprioritized servers is empty - return the primary.
// 2. deprioritized servers contains the primary - return the primary.
return servers.filter(primaryFilter);
case 'primaryPreferred': {
const primary = servers.filter(primaryFilter);
// If there is a primary and it is not deprioritized, use the primary. Otherwise,
// check for secondaries.
const eligiblePrimary = primary.filter(isDeprioritizedFactory(deprioritized));
if (eligiblePrimary.length) {
return eligiblePrimary;
}
// If we make it here, we either have:
// 1. a deprioritized primary
// 2. no eligible primary
// secondaries take precedence of deprioritized primaries.
const secondaries = tagSetReducer(
readPreference,
maxStalenessReducer(readPreference, topologyDescription, servers.filter(secondaryFilter))
);
const eligibleSecondaries = secondaries.filter(isDeprioritizedFactory(deprioritized));
if (eligibleSecondaries.length) {
return latencyWindowReducer(topologyDescription, eligibleSecondaries);
}
// if we make it here, we have no primaries or secondaries that not deprioritized.
// prefer the primary (which may not exist, if the topology has no primary).
// otherwise, return the secondaries (which also may not exist, but there is nothing else to check here).
return primary.length ? primary : latencyWindowReducer(topologyDescription, secondaries);
}
case 'nearest': {
const eligible = filterDeprioritized(
tagSetReducer(
readPreference,
maxStalenessReducer(readPreference, topologyDescription, servers.filter(nearestFilter))
),
deprioritized
);
return latencyWindowReducer(topologyDescription, eligible);
}
case 'secondary':
case 'secondaryPreferred': {
const secondaries = tagSetReducer(
readPreference,
maxStalenessReducer(readPreference, topologyDescription, servers.filter(secondaryFilter))
);
const eligibleSecondaries = secondaries.filter(isDeprioritizedFactory(deprioritized));
if (eligibleSecondaries.length) {
return latencyWindowReducer(topologyDescription, eligibleSecondaries);
}
// we have no eligible secondaries, try for a primary if we can.
if (mode === ReadPreference.SECONDARY_PREFERRED) {
const primary = servers.filter(primaryFilter);
// unlike readPreference=primary, here we do filter for deprioritized servers.
// if the primary is deprioritized, deprioritized secondaries take precedence.
const eligiblePrimary = primary.filter(isDeprioritizedFactory(deprioritized));
if (eligiblePrimary.length) return eligiblePrimary;
// we have no eligible primary nor secondaries that have not been deprioritized
return secondaries.length
? latencyWindowReducer(topologyDescription, secondaries)
: primary;
}
// return all secondaries in the latency window.
return latencyWindowReducer(topologyDescription, secondaries);
}
default: {
const _exhaustiveCheck: never = mode;
throw new MongoRuntimeError(
`unexpected readPreference=${mode} (should never happen). Please report a bug in the Node driver Jira project.`
);
}
}
}
/**
* Returns a function which selects servers based on a provided read preference
*
* @param readPreference - The read preference to select with
*/
export function readPreferenceServerSelector(readPreference: ReadPreference): ServerSelector {
if (!readPreference.isValid()) {
throw new MongoInvalidArgumentError('Invalid read preference specified');
}
return function readPreferenceServers(
topologyDescription: TopologyDescription,
servers: ServerDescription[],
deprioritized: DeprioritizedServers
): ServerDescription[] {
switch (topologyDescription.type) {
case 'Single':
return latencyWindowReducer(topologyDescription, servers.filter(knownFilter));
case 'ReplicaSetNoPrimary':
case 'ReplicaSetWithPrimary':
return secondarySelector(readPreference, topologyDescription, servers, deprioritized);
case 'Sharded': {
const selectable = filterDeprioritized(servers, deprioritized);
return latencyWindowReducer(topologyDescription, selectable.filter(knownFilter));
}
case 'Unknown':
return [];
case 'LoadBalanced':
return servers.filter(loadBalancerFilter);
default: {
const _exhaustiveCheck: never = topologyDescription.type;
throw new MongoRuntimeError(
`unexpected topology type: ${topologyDescription.type} (this should never happen). Please file a bug in the Node driver Jira project.`
);
}
}
};
}