You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
360 lines
18 KiB
JavaScript
360 lines
18 KiB
JavaScript
1 year ago
|
"use strict";
|
||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||
|
exports.TopologyDescription = void 0;
|
||
|
const WIRE_CONSTANTS = require("../cmap/wire_protocol/constants");
|
||
|
const error_1 = require("../error");
|
||
|
const utils_1 = require("../utils");
|
||
|
const common_1 = require("./common");
|
||
|
const server_description_1 = require("./server_description");
|
||
|
// constants related to compatibility checks
|
||
|
const MIN_SUPPORTED_SERVER_VERSION = WIRE_CONSTANTS.MIN_SUPPORTED_SERVER_VERSION;
|
||
|
const MAX_SUPPORTED_SERVER_VERSION = WIRE_CONSTANTS.MAX_SUPPORTED_SERVER_VERSION;
|
||
|
const MIN_SUPPORTED_WIRE_VERSION = WIRE_CONSTANTS.MIN_SUPPORTED_WIRE_VERSION;
|
||
|
const MAX_SUPPORTED_WIRE_VERSION = WIRE_CONSTANTS.MAX_SUPPORTED_WIRE_VERSION;
|
||
|
const MONGOS_OR_UNKNOWN = new Set([common_1.ServerType.Mongos, common_1.ServerType.Unknown]);
|
||
|
const MONGOS_OR_STANDALONE = new Set([common_1.ServerType.Mongos, common_1.ServerType.Standalone]);
|
||
|
const NON_PRIMARY_RS_MEMBERS = new Set([
|
||
|
common_1.ServerType.RSSecondary,
|
||
|
common_1.ServerType.RSArbiter,
|
||
|
common_1.ServerType.RSOther
|
||
|
]);
|
||
|
/**
|
||
|
* Representation of a deployment of servers
|
||
|
* @public
|
||
|
*/
|
||
|
class TopologyDescription {
|
||
|
/**
|
||
|
* Create a TopologyDescription
|
||
|
*/
|
||
|
constructor(topologyType, serverDescriptions = null, setName = null, maxSetVersion = null, maxElectionId = null, commonWireVersion = null, options = null) {
|
||
|
options = options ?? {};
|
||
|
this.type = topologyType ?? common_1.TopologyType.Unknown;
|
||
|
this.servers = serverDescriptions ?? new Map();
|
||
|
this.stale = false;
|
||
|
this.compatible = true;
|
||
|
this.heartbeatFrequencyMS = options.heartbeatFrequencyMS ?? 0;
|
||
|
this.localThresholdMS = options.localThresholdMS ?? 15;
|
||
|
this.setName = setName ?? null;
|
||
|
this.maxElectionId = maxElectionId ?? null;
|
||
|
this.maxSetVersion = maxSetVersion ?? null;
|
||
|
this.commonWireVersion = commonWireVersion ?? 0;
|
||
|
// determine server compatibility
|
||
|
for (const serverDescription of this.servers.values()) {
|
||
|
// Load balancer mode is always compatible.
|
||
|
if (serverDescription.type === common_1.ServerType.Unknown ||
|
||
|
serverDescription.type === common_1.ServerType.LoadBalancer) {
|
||
|
continue;
|
||
|
}
|
||
|
if (serverDescription.minWireVersion > MAX_SUPPORTED_WIRE_VERSION) {
|
||
|
this.compatible = false;
|
||
|
this.compatibilityError = `Server at ${serverDescription.address} requires wire version ${serverDescription.minWireVersion}, but this version of the driver only supports up to ${MAX_SUPPORTED_WIRE_VERSION} (MongoDB ${MAX_SUPPORTED_SERVER_VERSION})`;
|
||
|
}
|
||
|
if (serverDescription.maxWireVersion < MIN_SUPPORTED_WIRE_VERSION) {
|
||
|
this.compatible = false;
|
||
|
this.compatibilityError = `Server at ${serverDescription.address} reports wire version ${serverDescription.maxWireVersion}, but this version of the driver requires at least ${MIN_SUPPORTED_WIRE_VERSION} (MongoDB ${MIN_SUPPORTED_SERVER_VERSION}).`;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
// Whenever a client updates the TopologyDescription from a hello response, it MUST set
|
||
|
// TopologyDescription.logicalSessionTimeoutMinutes to the smallest logicalSessionTimeoutMinutes
|
||
|
// value among ServerDescriptions of all data-bearing server types. If any have a null
|
||
|
// logicalSessionTimeoutMinutes, then TopologyDescription.logicalSessionTimeoutMinutes MUST be
|
||
|
// set to null.
|
||
|
this.logicalSessionTimeoutMinutes = null;
|
||
|
for (const [, server] of this.servers) {
|
||
|
if (server.isReadable) {
|
||
|
if (server.logicalSessionTimeoutMinutes == null) {
|
||
|
// If any of the servers have a null logicalSessionsTimeout, then the whole topology does
|
||
|
this.logicalSessionTimeoutMinutes = null;
|
||
|
break;
|
||
|
}
|
||
|
if (this.logicalSessionTimeoutMinutes == null) {
|
||
|
// First server with a non null logicalSessionsTimeout
|
||
|
this.logicalSessionTimeoutMinutes = server.logicalSessionTimeoutMinutes;
|
||
|
continue;
|
||
|
}
|
||
|
// Always select the smaller of the:
|
||
|
// current server logicalSessionsTimeout and the topologies logicalSessionsTimeout
|
||
|
this.logicalSessionTimeoutMinutes = Math.min(this.logicalSessionTimeoutMinutes, server.logicalSessionTimeoutMinutes);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Returns a new TopologyDescription based on the SrvPollingEvent
|
||
|
* @internal
|
||
|
*/
|
||
|
updateFromSrvPollingEvent(ev, srvMaxHosts = 0) {
|
||
|
/** The SRV addresses defines the set of addresses we should be using */
|
||
|
const incomingHostnames = ev.hostnames();
|
||
|
const currentHostnames = new Set(this.servers.keys());
|
||
|
const hostnamesToAdd = new Set(incomingHostnames);
|
||
|
const hostnamesToRemove = new Set();
|
||
|
for (const hostname of currentHostnames) {
|
||
|
// filter hostnamesToAdd (made from incomingHostnames) down to what is *not* present in currentHostnames
|
||
|
hostnamesToAdd.delete(hostname);
|
||
|
if (!incomingHostnames.has(hostname)) {
|
||
|
// If the SRV Records no longer include this hostname
|
||
|
// we have to stop using it
|
||
|
hostnamesToRemove.add(hostname);
|
||
|
}
|
||
|
}
|
||
|
if (hostnamesToAdd.size === 0 && hostnamesToRemove.size === 0) {
|
||
|
// No new hosts to add and none to remove
|
||
|
return this;
|
||
|
}
|
||
|
const serverDescriptions = new Map(this.servers);
|
||
|
for (const removedHost of hostnamesToRemove) {
|
||
|
serverDescriptions.delete(removedHost);
|
||
|
}
|
||
|
if (hostnamesToAdd.size > 0) {
|
||
|
if (srvMaxHosts === 0) {
|
||
|
// Add all!
|
||
|
for (const hostToAdd of hostnamesToAdd) {
|
||
|
serverDescriptions.set(hostToAdd, new server_description_1.ServerDescription(hostToAdd));
|
||
|
}
|
||
|
}
|
||
|
else if (serverDescriptions.size < srvMaxHosts) {
|
||
|
// Add only the amount needed to get us back to srvMaxHosts
|
||
|
const selectedHosts = (0, utils_1.shuffle)(hostnamesToAdd, srvMaxHosts - serverDescriptions.size);
|
||
|
for (const selectedHostToAdd of selectedHosts) {
|
||
|
serverDescriptions.set(selectedHostToAdd, new server_description_1.ServerDescription(selectedHostToAdd));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return new TopologyDescription(this.type, serverDescriptions, this.setName, this.maxSetVersion, this.maxElectionId, this.commonWireVersion, { heartbeatFrequencyMS: this.heartbeatFrequencyMS, localThresholdMS: this.localThresholdMS });
|
||
|
}
|
||
|
/**
|
||
|
* Returns a copy of this description updated with a given ServerDescription
|
||
|
* @internal
|
||
|
*/
|
||
|
update(serverDescription) {
|
||
|
const address = serverDescription.address;
|
||
|
// potentially mutated values
|
||
|
let { type: topologyType, setName, maxSetVersion, maxElectionId, commonWireVersion } = this;
|
||
|
const serverType = serverDescription.type;
|
||
|
const serverDescriptions = new Map(this.servers);
|
||
|
// update common wire version
|
||
|
if (serverDescription.maxWireVersion !== 0) {
|
||
|
if (commonWireVersion == null) {
|
||
|
commonWireVersion = serverDescription.maxWireVersion;
|
||
|
}
|
||
|
else {
|
||
|
commonWireVersion = Math.min(commonWireVersion, serverDescription.maxWireVersion);
|
||
|
}
|
||
|
}
|
||
|
if (typeof serverDescription.setName === 'string' &&
|
||
|
typeof setName === 'string' &&
|
||
|
serverDescription.setName !== setName) {
|
||
|
if (topologyType === common_1.TopologyType.Single) {
|
||
|
// "Single" Topology with setName mismatch is direct connection usage, mark unknown do not remove
|
||
|
serverDescription = new server_description_1.ServerDescription(address);
|
||
|
}
|
||
|
else {
|
||
|
serverDescriptions.delete(address);
|
||
|
}
|
||
|
}
|
||
|
// update the actual server description
|
||
|
serverDescriptions.set(address, serverDescription);
|
||
|
if (topologyType === common_1.TopologyType.Single) {
|
||
|
// once we are defined as single, that never changes
|
||
|
return new TopologyDescription(common_1.TopologyType.Single, serverDescriptions, setName, maxSetVersion, maxElectionId, commonWireVersion, { heartbeatFrequencyMS: this.heartbeatFrequencyMS, localThresholdMS: this.localThresholdMS });
|
||
|
}
|
||
|
if (topologyType === common_1.TopologyType.Unknown) {
|
||
|
if (serverType === common_1.ServerType.Standalone && this.servers.size !== 1) {
|
||
|
serverDescriptions.delete(address);
|
||
|
}
|
||
|
else {
|
||
|
topologyType = topologyTypeForServerType(serverType);
|
||
|
}
|
||
|
}
|
||
|
if (topologyType === common_1.TopologyType.Sharded) {
|
||
|
if (!MONGOS_OR_UNKNOWN.has(serverType)) {
|
||
|
serverDescriptions.delete(address);
|
||
|
}
|
||
|
}
|
||
|
if (topologyType === common_1.TopologyType.ReplicaSetNoPrimary) {
|
||
|
if (MONGOS_OR_STANDALONE.has(serverType)) {
|
||
|
serverDescriptions.delete(address);
|
||
|
}
|
||
|
if (serverType === common_1.ServerType.RSPrimary) {
|
||
|
const result = updateRsFromPrimary(serverDescriptions, serverDescription, setName, maxSetVersion, maxElectionId);
|
||
|
topologyType = result[0];
|
||
|
setName = result[1];
|
||
|
maxSetVersion = result[2];
|
||
|
maxElectionId = result[3];
|
||
|
}
|
||
|
else if (NON_PRIMARY_RS_MEMBERS.has(serverType)) {
|
||
|
const result = updateRsNoPrimaryFromMember(serverDescriptions, serverDescription, setName);
|
||
|
topologyType = result[0];
|
||
|
setName = result[1];
|
||
|
}
|
||
|
}
|
||
|
if (topologyType === common_1.TopologyType.ReplicaSetWithPrimary) {
|
||
|
if (MONGOS_OR_STANDALONE.has(serverType)) {
|
||
|
serverDescriptions.delete(address);
|
||
|
topologyType = checkHasPrimary(serverDescriptions);
|
||
|
}
|
||
|
else if (serverType === common_1.ServerType.RSPrimary) {
|
||
|
const result = updateRsFromPrimary(serverDescriptions, serverDescription, setName, maxSetVersion, maxElectionId);
|
||
|
topologyType = result[0];
|
||
|
setName = result[1];
|
||
|
maxSetVersion = result[2];
|
||
|
maxElectionId = result[3];
|
||
|
}
|
||
|
else if (NON_PRIMARY_RS_MEMBERS.has(serverType)) {
|
||
|
topologyType = updateRsWithPrimaryFromMember(serverDescriptions, serverDescription, setName);
|
||
|
}
|
||
|
else {
|
||
|
topologyType = checkHasPrimary(serverDescriptions);
|
||
|
}
|
||
|
}
|
||
|
return new TopologyDescription(topologyType, serverDescriptions, setName, maxSetVersion, maxElectionId, commonWireVersion, { heartbeatFrequencyMS: this.heartbeatFrequencyMS, localThresholdMS: this.localThresholdMS });
|
||
|
}
|
||
|
get error() {
|
||
|
const descriptionsWithError = Array.from(this.servers.values()).filter((sd) => sd.error);
|
||
|
if (descriptionsWithError.length > 0) {
|
||
|
return descriptionsWithError[0].error;
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
/**
|
||
|
* Determines if the topology description has any known servers
|
||
|
*/
|
||
|
get hasKnownServers() {
|
||
|
return Array.from(this.servers.values()).some((sd) => sd.type !== common_1.ServerType.Unknown);
|
||
|
}
|
||
|
/**
|
||
|
* Determines if this topology description has a data-bearing server available.
|
||
|
*/
|
||
|
get hasDataBearingServers() {
|
||
|
return Array.from(this.servers.values()).some((sd) => sd.isDataBearing);
|
||
|
}
|
||
|
/**
|
||
|
* Determines if the topology has a definition for the provided address
|
||
|
* @internal
|
||
|
*/
|
||
|
hasServer(address) {
|
||
|
return this.servers.has(address);
|
||
|
}
|
||
|
}
|
||
|
exports.TopologyDescription = TopologyDescription;
|
||
|
function topologyTypeForServerType(serverType) {
|
||
|
switch (serverType) {
|
||
|
case common_1.ServerType.Standalone:
|
||
|
return common_1.TopologyType.Single;
|
||
|
case common_1.ServerType.Mongos:
|
||
|
return common_1.TopologyType.Sharded;
|
||
|
case common_1.ServerType.RSPrimary:
|
||
|
return common_1.TopologyType.ReplicaSetWithPrimary;
|
||
|
case common_1.ServerType.RSOther:
|
||
|
case common_1.ServerType.RSSecondary:
|
||
|
return common_1.TopologyType.ReplicaSetNoPrimary;
|
||
|
default:
|
||
|
return common_1.TopologyType.Unknown;
|
||
|
}
|
||
|
}
|
||
|
function updateRsFromPrimary(serverDescriptions, serverDescription, setName = null, maxSetVersion = null, maxElectionId = null) {
|
||
|
setName = setName || serverDescription.setName;
|
||
|
if (setName !== serverDescription.setName) {
|
||
|
serverDescriptions.delete(serverDescription.address);
|
||
|
return [checkHasPrimary(serverDescriptions), setName, maxSetVersion, maxElectionId];
|
||
|
}
|
||
|
if (serverDescription.maxWireVersion >= 17) {
|
||
|
const electionIdComparison = (0, utils_1.compareObjectId)(maxElectionId, serverDescription.electionId);
|
||
|
const maxElectionIdIsEqual = electionIdComparison === 0;
|
||
|
const maxElectionIdIsLess = electionIdComparison === -1;
|
||
|
const maxSetVersionIsLessOrEqual = (maxSetVersion ?? -1) <= (serverDescription.setVersion ?? -1);
|
||
|
if (maxElectionIdIsLess || (maxElectionIdIsEqual && maxSetVersionIsLessOrEqual)) {
|
||
|
// The reported electionId was greater
|
||
|
// or the electionId was equal and reported setVersion was greater
|
||
|
// Always update both values, they are a tuple
|
||
|
maxElectionId = serverDescription.electionId;
|
||
|
maxSetVersion = serverDescription.setVersion;
|
||
|
}
|
||
|
else {
|
||
|
// Stale primary
|
||
|
// replace serverDescription with a default ServerDescription of type "Unknown"
|
||
|
serverDescriptions.set(serverDescription.address, new server_description_1.ServerDescription(serverDescription.address));
|
||
|
return [checkHasPrimary(serverDescriptions), setName, maxSetVersion, maxElectionId];
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
const electionId = serverDescription.electionId ? serverDescription.electionId : null;
|
||
|
if (serverDescription.setVersion && electionId) {
|
||
|
if (maxSetVersion && maxElectionId) {
|
||
|
if (maxSetVersion > serverDescription.setVersion ||
|
||
|
(0, utils_1.compareObjectId)(maxElectionId, electionId) > 0) {
|
||
|
// this primary is stale, we must remove it
|
||
|
serverDescriptions.set(serverDescription.address, new server_description_1.ServerDescription(serverDescription.address));
|
||
|
return [checkHasPrimary(serverDescriptions), setName, maxSetVersion, maxElectionId];
|
||
|
}
|
||
|
}
|
||
|
maxElectionId = serverDescription.electionId;
|
||
|
}
|
||
|
if (serverDescription.setVersion != null &&
|
||
|
(maxSetVersion == null || serverDescription.setVersion > maxSetVersion)) {
|
||
|
maxSetVersion = serverDescription.setVersion;
|
||
|
}
|
||
|
}
|
||
|
// We've heard from the primary. Is it the same primary as before?
|
||
|
for (const [address, server] of serverDescriptions) {
|
||
|
if (server.type === common_1.ServerType.RSPrimary && server.address !== serverDescription.address) {
|
||
|
// Reset old primary's type to Unknown.
|
||
|
serverDescriptions.set(address, new server_description_1.ServerDescription(server.address));
|
||
|
// There can only be one primary
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
// Discover new hosts from this primary's response.
|
||
|
serverDescription.allHosts.forEach((address) => {
|
||
|
if (!serverDescriptions.has(address)) {
|
||
|
serverDescriptions.set(address, new server_description_1.ServerDescription(address));
|
||
|
}
|
||
|
});
|
||
|
// Remove hosts not in the response.
|
||
|
const currentAddresses = Array.from(serverDescriptions.keys());
|
||
|
const responseAddresses = serverDescription.allHosts;
|
||
|
currentAddresses
|
||
|
.filter((addr) => responseAddresses.indexOf(addr) === -1)
|
||
|
.forEach((address) => {
|
||
|
serverDescriptions.delete(address);
|
||
|
});
|
||
|
return [checkHasPrimary(serverDescriptions), setName, maxSetVersion, maxElectionId];
|
||
|
}
|
||
|
function updateRsWithPrimaryFromMember(serverDescriptions, serverDescription, setName = null) {
|
||
|
if (setName == null) {
|
||
|
// TODO(NODE-3483): should be an appropriate runtime error
|
||
|
throw new error_1.MongoRuntimeError('Argument "setName" is required if connected to a replica set');
|
||
|
}
|
||
|
if (setName !== serverDescription.setName ||
|
||
|
(serverDescription.me && serverDescription.address !== serverDescription.me)) {
|
||
|
serverDescriptions.delete(serverDescription.address);
|
||
|
}
|
||
|
return checkHasPrimary(serverDescriptions);
|
||
|
}
|
||
|
function updateRsNoPrimaryFromMember(serverDescriptions, serverDescription, setName = null) {
|
||
|
const topologyType = common_1.TopologyType.ReplicaSetNoPrimary;
|
||
|
setName = setName ?? serverDescription.setName;
|
||
|
if (setName !== serverDescription.setName) {
|
||
|
serverDescriptions.delete(serverDescription.address);
|
||
|
return [topologyType, setName];
|
||
|
}
|
||
|
serverDescription.allHosts.forEach((address) => {
|
||
|
if (!serverDescriptions.has(address)) {
|
||
|
serverDescriptions.set(address, new server_description_1.ServerDescription(address));
|
||
|
}
|
||
|
});
|
||
|
if (serverDescription.me && serverDescription.address !== serverDescription.me) {
|
||
|
serverDescriptions.delete(serverDescription.address);
|
||
|
}
|
||
|
return [topologyType, setName];
|
||
|
}
|
||
|
function checkHasPrimary(serverDescriptions) {
|
||
|
for (const serverDescription of serverDescriptions.values()) {
|
||
|
if (serverDescription.type === common_1.ServerType.RSPrimary) {
|
||
|
return common_1.TopologyType.ReplicaSetWithPrimary;
|
||
|
}
|
||
|
}
|
||
|
return common_1.TopologyType.ReplicaSetNoPrimary;
|
||
|
}
|
||
|
//# sourceMappingURL=topology_description.js.map
|