@skybloxsystems/ticket-bot
Version:
280 lines (236 loc) • 8.01 kB
text/typescript
import { arrayStrictEqual, errorStrictEqual, now, HostAddress } from '../utils';
import { ServerType } from './common';
import { ObjectId, Long, Document } from '../bson';
import type { ClusterTime } from './common';
import type { MongoError } from '../error';
const WRITABLE_SERVER_TYPES = new Set<ServerType>([
ServerType.RSPrimary,
ServerType.Standalone,
ServerType.Mongos,
ServerType.LoadBalancer
]);
const DATA_BEARING_SERVER_TYPES = new Set<ServerType>([
ServerType.RSPrimary,
ServerType.RSSecondary,
ServerType.Mongos,
ServerType.Standalone,
ServerType.LoadBalancer
]);
/** @public */
export interface TopologyVersion {
processId: ObjectId;
counter: Long;
}
/** @public */
export type TagSet = { [key: string]: string };
/** @internal */
export interface ServerDescriptionOptions {
/** An Error used for better reporting debugging */
error?: MongoError;
/** The round trip time to ping this server (in ms) */
roundTripTime?: number;
/** The topologyVersion */
topologyVersion?: TopologyVersion;
/** If the client is in load balancing mode. */
loadBalanced?: boolean;
}
/**
* The client's view of a single server, based on the most recent ismaster outcome.
*
* Internal type, not meant to be directly instantiated
* @public
*/
export class ServerDescription {
private _hostAddress: HostAddress;
address: string;
type: ServerType;
hosts: string[];
passives: string[];
arbiters: string[];
tags: TagSet;
error?: MongoError;
topologyVersion?: TopologyVersion;
minWireVersion: number;
maxWireVersion: number;
roundTripTime: number;
lastUpdateTime: number;
lastWriteDate: number;
me?: string;
primary?: string;
setName?: string;
setVersion?: number;
electionId?: ObjectId;
logicalSessionTimeoutMinutes?: number;
// NOTE: does this belong here? It seems we should gossip the cluster time at the CMAP level
$clusterTime?: ClusterTime;
/**
* Create a ServerDescription
* @internal
*
* @param address - The address of the server
* @param ismaster - An optional ismaster response for this server
*/
constructor(
address: HostAddress | string,
ismaster?: Document,
options?: ServerDescriptionOptions
) {
if (typeof address === 'string') {
this._hostAddress = new HostAddress(address);
this.address = this._hostAddress.toString();
} else {
this._hostAddress = address;
this.address = this._hostAddress.toString();
}
this.type = parseServerType(ismaster, options);
this.hosts = ismaster?.hosts?.map((host: string) => host.toLowerCase()) ?? [];
this.passives = ismaster?.passives?.map((host: string) => host.toLowerCase()) ?? [];
this.arbiters = ismaster?.arbiters?.map((host: string) => host.toLowerCase()) ?? [];
this.tags = ismaster?.tags ?? {};
this.minWireVersion = ismaster?.minWireVersion ?? 0;
this.maxWireVersion = ismaster?.maxWireVersion ?? 0;
this.roundTripTime = options?.roundTripTime ?? -1;
this.lastUpdateTime = now();
this.lastWriteDate = ismaster?.lastWrite?.lastWriteDate ?? 0;
if (options?.topologyVersion) {
this.topologyVersion = options.topologyVersion;
} else if (ismaster?.topologyVersion) {
this.topologyVersion = ismaster.topologyVersion;
}
if (options?.error) {
this.error = options.error;
}
if (ismaster?.primary) {
this.primary = ismaster.primary;
}
if (ismaster?.me) {
this.me = ismaster.me.toLowerCase();
}
if (ismaster?.setName) {
this.setName = ismaster.setName;
}
if (ismaster?.setVersion) {
this.setVersion = ismaster.setVersion;
}
if (ismaster?.electionId) {
this.electionId = ismaster.electionId;
}
if (ismaster?.logicalSessionTimeoutMinutes) {
this.logicalSessionTimeoutMinutes = ismaster.logicalSessionTimeoutMinutes;
}
if (ismaster?.$clusterTime) {
this.$clusterTime = ismaster.$clusterTime;
}
}
get hostAddress(): HostAddress {
if (this._hostAddress) return this._hostAddress;
else return new HostAddress(this.address);
}
get allHosts(): string[] {
return this.hosts.concat(this.arbiters).concat(this.passives);
}
/** Is this server available for reads*/
get isReadable(): boolean {
return this.type === ServerType.RSSecondary || this.isWritable;
}
/** Is this server data bearing */
get isDataBearing(): boolean {
return DATA_BEARING_SERVER_TYPES.has(this.type);
}
/** Is this server available for writes */
get isWritable(): boolean {
return WRITABLE_SERVER_TYPES.has(this.type);
}
get host(): string {
const chopLength = `:${this.port}`.length;
return this.address.slice(0, -chopLength);
}
get port(): number {
const port = this.address.split(':').pop();
return port ? Number.parseInt(port, 10) : 27017;
}
/**
* Determines if another `ServerDescription` is equal to this one per the rules defined
* in the {@link https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring.rst#serverdescription|SDAM spec}
*/
equals(other: ServerDescription): boolean {
const topologyVersionsEqual =
this.topologyVersion === other.topologyVersion ||
compareTopologyVersion(this.topologyVersion, other.topologyVersion) === 0;
const electionIdsEqual: boolean =
this.electionId && other.electionId
? other.electionId && this.electionId.equals(other.electionId)
: this.electionId === other.electionId;
return (
other != null &&
errorStrictEqual(this.error, other.error) &&
this.type === other.type &&
this.minWireVersion === other.minWireVersion &&
arrayStrictEqual(this.hosts, other.hosts) &&
tagsStrictEqual(this.tags, other.tags) &&
this.setName === other.setName &&
this.setVersion === other.setVersion &&
electionIdsEqual &&
this.primary === other.primary &&
this.logicalSessionTimeoutMinutes === other.logicalSessionTimeoutMinutes &&
topologyVersionsEqual
);
}
}
// Parses an `ismaster` message and determines the server type
export function parseServerType(
ismaster?: Document,
options?: ServerDescriptionOptions
): ServerType {
if (options?.loadBalanced) {
return ServerType.LoadBalancer;
}
if (!ismaster || !ismaster.ok) {
return ServerType.Unknown;
}
if (ismaster.isreplicaset) {
return ServerType.RSGhost;
}
if (ismaster.msg && ismaster.msg === 'isdbgrid') {
return ServerType.Mongos;
}
if (ismaster.setName) {
if (ismaster.hidden) {
return ServerType.RSOther;
} else if (ismaster.ismaster || ismaster.isWritablePrimary) {
return ServerType.RSPrimary;
} else if (ismaster.secondary) {
return ServerType.RSSecondary;
} else if (ismaster.arbiterOnly) {
return ServerType.RSArbiter;
} else {
return ServerType.RSOther;
}
}
return ServerType.Standalone;
}
function tagsStrictEqual(tags: TagSet, tags2: TagSet): boolean {
const tagsKeys = Object.keys(tags);
const tags2Keys = Object.keys(tags2);
return (
tagsKeys.length === tags2Keys.length &&
tagsKeys.every((key: string) => tags2[key] === tags[key])
);
}
/**
* Compares two topology versions.
*
* @returns A negative number if `lhs` is older than `rhs`; positive if `lhs` is newer than `rhs`; 0 if they are equivalent.
*/
export function compareTopologyVersion(lhs?: TopologyVersion, rhs?: TopologyVersion): number {
if (lhs == null || rhs == null) {
return -1;
}
if (lhs.processId.equals(rhs.processId)) {
// tests mock counter as just number, but in a real situation counter should always be a Long
const lhsCounter = Long.isLong(lhs.counter) ? lhs.counter : Long.fromNumber(lhs.counter);
const rhsCounter = Long.isLong(rhs.counter) ? lhs.counter : Long.fromNumber(rhs.counter);
return lhsCounter.compare(rhsCounter);
}
return -1;
}