CoolFace
Apppublic

opusdev/vector-similarity-api

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes
monitor.ts772 linesDownload Raw Back to sdam
1import { clearTimeout, setTimeout } from 'timers';2 3import { type Document, Long } from '../bson';4import { connect, makeConnection, makeSocket, performInitialHandshake } from '../cmap/connect';5import type { Connection, ConnectionOptions } from '../cmap/connection';6import { getFAASEnv } from '../cmap/handshake/client_metadata';7import { LEGACY_HELLO_COMMAND } from '../constants';8import { MongoError, MongoErrorLabel, MongoNetworkTimeoutError } from '../error';9import { MongoLoggableComponent } from '../mongo_logger';10import { CancellationToken, TypedEventEmitter } from '../mongo_types';11import {12  calculateDurationInMs,13  type Callback,14  type EventEmitterWithState,15  makeStateMachine,16  noop,17  now,18  ns19} from '../utils';20import { ServerType, STATE_CLOSED, STATE_CLOSING } from './common';21import {22  ServerHeartbeatFailedEvent,23  ServerHeartbeatStartedEvent,24  ServerHeartbeatSucceededEvent25} from './events';26import { Server } from './server';27import type { TopologyVersion } from './server_description';28 29const STATE_IDLE = 'idle';30const STATE_MONITORING = 'monitoring';31const stateTransition = makeStateMachine({32  [STATE_CLOSING]: [STATE_CLOSING, STATE_IDLE, STATE_CLOSED],33  [STATE_CLOSED]: [STATE_CLOSED, STATE_MONITORING],34  [STATE_IDLE]: [STATE_IDLE, STATE_MONITORING, STATE_CLOSING],35  [STATE_MONITORING]: [STATE_MONITORING, STATE_IDLE, STATE_CLOSING]36});37 38const INVALID_REQUEST_CHECK_STATES = new Set([STATE_CLOSING, STATE_CLOSED, STATE_MONITORING]);39function isInCloseState(monitor: Monitor) {40  return monitor.s.state === STATE_CLOSED || monitor.s.state === STATE_CLOSING;41}42 43/** @public */44export const ServerMonitoringMode = Object.freeze({45  auto: 'auto',46  poll: 'poll',47  stream: 'stream'48} as const);49 50/** @public */51export type ServerMonitoringMode = (typeof ServerMonitoringMode)[keyof typeof ServerMonitoringMode];52 53/** @internal */54export interface MonitorPrivate {55  state: string;56}57 58/** @public */59export interface MonitorOptions60  extends Omit<ConnectionOptions, 'id' | 'generation' | 'hostAddress'> {61  connectTimeoutMS: number;62  heartbeatFrequencyMS: number;63  minHeartbeatFrequencyMS: number;64  serverMonitoringMode: ServerMonitoringMode;65}66 67/** @public */68export type MonitorEvents = {69  serverHeartbeatStarted(event: ServerHeartbeatStartedEvent): void;70  serverHeartbeatSucceeded(event: ServerHeartbeatSucceededEvent): void;71  serverHeartbeatFailed(event: ServerHeartbeatFailedEvent): void;72  resetServer(error?: MongoError): void;73  resetConnectionPool(): void;74  close(): void;75} & EventEmitterWithState;76 77/** @internal */78export class Monitor extends TypedEventEmitter<MonitorEvents> {79  /** @internal */80  s: MonitorPrivate;81  address: string;82  options: Readonly<83    Pick<84      MonitorOptions,85      | 'connectTimeoutMS'86      | 'heartbeatFrequencyMS'87      | 'minHeartbeatFrequencyMS'88      | 'serverMonitoringMode'89    >90  >;91  connectOptions: ConnectionOptions;92  isRunningInFaasEnv: boolean;93  server: Server;94  connection: Connection | null;95  cancellationToken: CancellationToken;96  /** @internal */97  monitorId?: MonitorInterval;98  rttPinger?: RTTPinger;99  /** @internal */100  override component = MongoLoggableComponent.TOPOLOGY;101  /** @internal */102  private rttSampler: RTTSampler;103 104  constructor(server: Server, options: MonitorOptions) {105    super();106    this.on('error', noop);107 108    this.server = server;109    this.connection = null;110    this.cancellationToken = new CancellationToken();111    this.cancellationToken.setMaxListeners(Infinity);112    this.monitorId = undefined;113    this.s = {114      state: STATE_CLOSED115    };116    this.address = server.description.address;117    this.options = Object.freeze({118      connectTimeoutMS: options.connectTimeoutMS ?? 10000,119      heartbeatFrequencyMS: options.heartbeatFrequencyMS ?? 10000,120      minHeartbeatFrequencyMS: options.minHeartbeatFrequencyMS ?? 500,121      serverMonitoringMode: options.serverMonitoringMode122    });123    this.isRunningInFaasEnv = getFAASEnv() != null;124    this.mongoLogger = this.server.topology.client?.mongoLogger;125    this.rttSampler = new RTTSampler(10);126 127    const cancellationToken = this.cancellationToken;128    // TODO: refactor this to pull it directly from the pool, requires new ConnectionPool integration129    const connectOptions = {130      id: '<monitor>' as const,131      generation: server.pool.generation,132      cancellationToken,133      hostAddress: server.description.hostAddress,134      ...options,135      // force BSON serialization options136      raw: false,137      useBigInt64: false,138      promoteLongs: true,139      promoteValues: true,140      promoteBuffers: true141    };142 143    // ensure no authentication is used for monitoring144    delete connectOptions.credentials;145    if (connectOptions.autoEncrypter) {146      delete connectOptions.autoEncrypter;147    }148 149    this.connectOptions = Object.freeze(connectOptions);150  }151 152  connect(): void {153    if (this.s.state !== STATE_CLOSED) {154      return;155    }156 157    // start158    const heartbeatFrequencyMS = this.options.heartbeatFrequencyMS;159    const minHeartbeatFrequencyMS = this.options.minHeartbeatFrequencyMS;160    this.monitorId = new MonitorInterval(monitorServer(this), {161      heartbeatFrequencyMS: heartbeatFrequencyMS,162      minHeartbeatFrequencyMS: minHeartbeatFrequencyMS,163      immediate: true164    });165  }166 167  requestCheck(): void {168    if (INVALID_REQUEST_CHECK_STATES.has(this.s.state)) {169      return;170    }171 172    this.monitorId?.wake();173  }174 175  reset(): void {176    const topologyVersion = this.server.description.topologyVersion;177    if (isInCloseState(this) || topologyVersion == null) {178      return;179    }180 181    stateTransition(this, STATE_CLOSING);182    resetMonitorState(this);183 184    // restart monitor185    stateTransition(this, STATE_IDLE);186 187    // restart monitoring188    const heartbeatFrequencyMS = this.options.heartbeatFrequencyMS;189    const minHeartbeatFrequencyMS = this.options.minHeartbeatFrequencyMS;190    this.monitorId = new MonitorInterval(monitorServer(this), {191      heartbeatFrequencyMS: heartbeatFrequencyMS,192      minHeartbeatFrequencyMS: minHeartbeatFrequencyMS193    });194  }195 196  close(): void {197    if (isInCloseState(this)) {198      return;199    }200 201    stateTransition(this, STATE_CLOSING);202    resetMonitorState(this);203 204    // close monitor205    this.emit('close');206    stateTransition(this, STATE_CLOSED);207  }208 209  get roundTripTime(): number {210    return this.rttSampler.average();211  }212 213  get minRoundTripTime(): number {214    return this.rttSampler.min();215  }216 217  get latestRtt(): number | null {218    return this.rttSampler.last;219  }220 221  addRttSample(rtt: number) {222    this.rttSampler.addSample(rtt);223  }224 225  clearRttSamples() {226    this.rttSampler.clear();227  }228}229 230function resetMonitorState(monitor: Monitor) {231  monitor.monitorId?.stop();232  monitor.monitorId = undefined;233 234  monitor.rttPinger?.close();235  monitor.rttPinger = undefined;236 237  monitor.cancellationToken.emit('cancel');238 239  monitor.connection?.destroy();240  monitor.connection = null;241 242  monitor.clearRttSamples();243}244 245function useStreamingProtocol(monitor: Monitor, topologyVersion: TopologyVersion | null): boolean {246  // If we have no topology version we always poll no matter247  // what the user provided, since the server does not support248  // the streaming protocol.249  if (topologyVersion == null) return false;250 251  const serverMonitoringMode = monitor.options.serverMonitoringMode;252  if (serverMonitoringMode === ServerMonitoringMode.poll) return false;253  if (serverMonitoringMode === ServerMonitoringMode.stream) return true;254 255  // If we are in auto mode, we need to figure out if we're in a FaaS256  // environment or not and choose the appropriate mode.257  if (monitor.isRunningInFaasEnv) return false;258  return true;259}260 261function checkServer(monitor: Monitor, callback: Callback<Document | null>) {262  let start: number;263  let awaited: boolean;264  const topologyVersion = monitor.server.description.topologyVersion;265  const isAwaitable = useStreamingProtocol(monitor, topologyVersion);266  monitor.emitAndLogHeartbeat(267    Server.SERVER_HEARTBEAT_STARTED,268    monitor.server.topology.s.id,269    undefined,270    new ServerHeartbeatStartedEvent(monitor.address, isAwaitable)271  );272 273  function onHeartbeatFailed(err: Error) {274    monitor.connection?.destroy();275    monitor.connection = null;276    monitor.emitAndLogHeartbeat(277      Server.SERVER_HEARTBEAT_FAILED,278      monitor.server.topology.s.id,279      undefined,280      new ServerHeartbeatFailedEvent(monitor.address, calculateDurationInMs(start), err, awaited)281    );282 283    const error = !(err instanceof MongoError)284      ? new MongoError(MongoError.buildErrorMessage(err), { cause: err })285      : err;286    error.addErrorLabel(MongoErrorLabel.ResetPool);287    if (error instanceof MongoNetworkTimeoutError) {288      error.addErrorLabel(MongoErrorLabel.InterruptInUseConnections);289    }290 291    monitor.emit('resetServer', error);292    callback(err);293  }294 295  function onHeartbeatSucceeded(hello: Document) {296    if (!('isWritablePrimary' in hello)) {297      // Provide hello-style response document.298      hello.isWritablePrimary = hello[LEGACY_HELLO_COMMAND];299    }300 301    // NOTE: here we use the latestRtt as this measurement corresponds with the value302    // obtained for this successful heartbeat, if there is no latestRtt, then we calculate the303    // duration304    const duration =305      isAwaitable && monitor.rttPinger306        ? (monitor.rttPinger.latestRtt ?? calculateDurationInMs(start))307        : calculateDurationInMs(start);308 309    monitor.addRttSample(duration);310 311    monitor.emitAndLogHeartbeat(312      Server.SERVER_HEARTBEAT_SUCCEEDED,313      monitor.server.topology.s.id,314      hello.connectionId,315      new ServerHeartbeatSucceededEvent(monitor.address, duration, hello, isAwaitable)316    );317 318    if (isAwaitable) {319      // If we are using the streaming protocol then we immediately issue another 'started'320      // event, otherwise the "check" is complete and return to the main monitor loop321      monitor.emitAndLogHeartbeat(322        Server.SERVER_HEARTBEAT_STARTED,323        monitor.server.topology.s.id,324        undefined,325        new ServerHeartbeatStartedEvent(monitor.address, true)326      );327      // We have not actually sent an outgoing handshake, but when we get the next response we328      // want the duration to reflect the time since we last heard from the server329      start = now();330    } else {331      monitor.rttPinger?.close();332      monitor.rttPinger = undefined;333 334      callback(undefined, hello);335    }336  }337 338  const { connection } = monitor;339  if (connection && !connection.closed) {340    const { serverApi, helloOk } = connection;341    const connectTimeoutMS = monitor.options.connectTimeoutMS;342    const maxAwaitTimeMS = monitor.options.heartbeatFrequencyMS;343 344    const cmd = {345      [serverApi?.version || helloOk ? 'hello' : LEGACY_HELLO_COMMAND]: 1,346      ...(isAwaitable && topologyVersion347        ? { maxAwaitTimeMS, topologyVersion: makeTopologyVersion(topologyVersion) }348        : {})349    };350 351    const options = isAwaitable352      ? {353          socketTimeoutMS: connectTimeoutMS ? connectTimeoutMS + maxAwaitTimeMS : 0,354          exhaustAllowed: true355        }356      : { socketTimeoutMS: connectTimeoutMS };357 358    if (isAwaitable && monitor.rttPinger == null) {359      monitor.rttPinger = new RTTPinger(monitor);360    }361 362    // Record new start time before sending handshake363    start = now();364 365    if (isAwaitable) {366      awaited = true;367      return connection.exhaustCommand(ns('admin.$cmd'), cmd, options, (error, hello) => {368        if (error) return onHeartbeatFailed(error);369        return onHeartbeatSucceeded(hello);370      });371    }372 373    awaited = false;374    connection375      .command(ns('admin.$cmd'), cmd, options)376      .then(onHeartbeatSucceeded, onHeartbeatFailed);377 378    return;379  }380 381  // connecting does an implicit `hello`382  (async () => {383    const socket = await makeSocket(monitor.connectOptions);384    const connection = makeConnection(monitor.connectOptions, socket);385    // The start time is after socket creation but before the handshake386    start = now();387    try {388      await performInitialHandshake(connection, monitor.connectOptions);389      return connection;390    } catch (error) {391      connection.destroy();392      throw error;393    }394  })().then(395    connection => {396      if (isInCloseState(monitor)) {397        connection.destroy();398        return;399      }400      const duration = calculateDurationInMs(start);401      monitor.addRttSample(duration);402 403      monitor.connection = connection;404      monitor.emitAndLogHeartbeat(405        Server.SERVER_HEARTBEAT_SUCCEEDED,406        monitor.server.topology.s.id,407        connection.hello?.connectionId,408        new ServerHeartbeatSucceededEvent(409          monitor.address,410          duration,411          connection.hello,412          useStreamingProtocol(monitor, connection.hello?.topologyVersion)413        )414      );415 416      callback(undefined, connection.hello);417    },418    error => {419      monitor.connection = null;420      awaited = false;421      onHeartbeatFailed(error);422    }423  );424}425 426function monitorServer(monitor: Monitor) {427  return (callback: Callback) => {428    if (monitor.s.state === STATE_MONITORING) {429      process.nextTick(callback);430      return;431    }432    stateTransition(monitor, STATE_MONITORING);433    function done() {434      if (!isInCloseState(monitor)) {435        stateTransition(monitor, STATE_IDLE);436      }437 438      callback();439    }440 441    checkServer(monitor, (err, hello) => {442      if (err) {443        // otherwise an error occurred on initial discovery, also bail444        if (monitor.server.description.type === ServerType.Unknown) {445          return done();446        }447      }448 449      // if the check indicates streaming is supported, immediately reschedule monitoring450      if (useStreamingProtocol(monitor, hello?.topologyVersion)) {451        setTimeout(() => {452          if (!isInCloseState(monitor)) {453            monitor.monitorId?.wake();454          }455        }, 0);456      }457 458      done();459    });460  };461}462 463function makeTopologyVersion(tv: TopologyVersion) {464  return {465    processId: tv.processId,466    // tests mock counter as just number, but in a real situation counter should always be a Long467    // TODO(NODE-2674): Preserve int64 sent from MongoDB468    counter: Long.isLong(tv.counter) ? tv.counter : Long.fromNumber(tv.counter)469  };470}471 472/** @internal */473export interface RTTPingerOptions extends ConnectionOptions {474  heartbeatFrequencyMS: number;475}476 477/** @internal */478export class RTTPinger {479  connection?: Connection;480  /** @internal */481  cancellationToken: CancellationToken;482  /** @internal */483  monitorId: NodeJS.Timeout;484  /** @internal */485  monitor: Monitor;486  closed: boolean;487  /** @internal */488  latestRtt?: number;489 490  constructor(monitor: Monitor) {491    this.connection = undefined;492    this.cancellationToken = monitor.cancellationToken;493    this.closed = false;494    this.monitor = monitor;495    this.latestRtt = monitor.latestRtt ?? undefined;496 497    const heartbeatFrequencyMS = monitor.options.heartbeatFrequencyMS;498    this.monitorId = setTimeout(() => this.measureRoundTripTime(), heartbeatFrequencyMS);499  }500 501  get roundTripTime(): number {502    return this.monitor.roundTripTime;503  }504 505  get minRoundTripTime(): number {506    return this.monitor.minRoundTripTime;507  }508 509  close(): void {510    this.closed = true;511    clearTimeout(this.monitorId);512 513    this.connection?.destroy();514    this.connection = undefined;515  }516 517  private measureAndReschedule(start: number, conn?: Connection) {518    if (this.closed) {519      conn?.destroy();520      return;521    }522 523    if (this.connection == null) {524      this.connection = conn;525    }526 527    this.latestRtt = calculateDurationInMs(start);528    this.monitorId = setTimeout(529      () => this.measureRoundTripTime(),530      this.monitor.options.heartbeatFrequencyMS531    );532  }533 534  private measureRoundTripTime() {535    const start = now();536 537    if (this.closed) {538      return;539    }540 541    const connection = this.connection;542    if (connection == null) {543      connect(this.monitor.connectOptions).then(544        connection => {545          this.measureAndReschedule(start, connection);546        },547        () => {548          this.connection = undefined;549        }550      );551      return;552    }553 554    const commandName =555      connection.serverApi?.version || connection.helloOk ? 'hello' : LEGACY_HELLO_COMMAND;556 557    connection.command(ns('admin.$cmd'), { [commandName]: 1 }, undefined).then(558      () => this.measureAndReschedule(start),559      () => {560        this.connection?.destroy();561        this.connection = undefined;562        return;563      }564    );565  }566}567 568/**569 * @internal570 */571export interface MonitorIntervalOptions {572  /** The interval to execute a method on */573  heartbeatFrequencyMS: number;574  /** A minimum interval that must elapse before the method is called */575  minHeartbeatFrequencyMS: number;576  /** Whether the method should be called immediately when the interval is started  */577  immediate: boolean;578}579 580/**581 * @internal582 */583export class MonitorInterval {584  fn: (callback: Callback) => void;585  timerId: NodeJS.Timeout | undefined;586  lastExecutionEnded: number;587  isExpeditedCallToFnScheduled = false;588  stopped = false;589  isExecutionInProgress = false;590  hasExecutedOnce = false;591 592  heartbeatFrequencyMS: number;593  minHeartbeatFrequencyMS: number;594 595  constructor(fn: (callback: Callback) => void, options: Partial<MonitorIntervalOptions> = {}) {596    this.fn = fn;597    this.lastExecutionEnded = -Infinity;598 599    this.heartbeatFrequencyMS = options.heartbeatFrequencyMS ?? 1000;600    this.minHeartbeatFrequencyMS = options.minHeartbeatFrequencyMS ?? 500;601 602    if (options.immediate) {603      this._executeAndReschedule();604    } else {605      this._reschedule(undefined);606    }607  }608 609  wake() {610    const currentTime = now();611    const timeSinceLastCall = currentTime - this.lastExecutionEnded;612 613    // TODO(NODE-4674): Add error handling and logging to the monitor614    if (timeSinceLastCall < 0) {615      return this._executeAndReschedule();616    }617 618    if (this.isExecutionInProgress) {619      return;620    }621 622    // debounce multiple calls to wake within the `minInterval`623    if (this.isExpeditedCallToFnScheduled) {624      return;625    }626 627    // reschedule a call as soon as possible, ensuring the call never happens628    // faster than the `minInterval`629    if (timeSinceLastCall < this.minHeartbeatFrequencyMS) {630      this.isExpeditedCallToFnScheduled = true;631      this._reschedule(this.minHeartbeatFrequencyMS - timeSinceLastCall);632      return;633    }634 635    this._executeAndReschedule();636  }637 638  stop() {639    this.stopped = true;640    if (this.timerId) {641      clearTimeout(this.timerId);642      this.timerId = undefined;643    }644 645    this.lastExecutionEnded = -Infinity;646    this.isExpeditedCallToFnScheduled = false;647  }648 649  toString() {650    return JSON.stringify(this);651  }652 653  toJSON() {654    const currentTime = now();655    const timeSinceLastCall = currentTime - this.lastExecutionEnded;656    return {657      timerId: this.timerId != null ? 'set' : 'cleared',658      lastCallTime: this.lastExecutionEnded,659      isExpeditedCheckScheduled: this.isExpeditedCallToFnScheduled,660      stopped: this.stopped,661      heartbeatFrequencyMS: this.heartbeatFrequencyMS,662      minHeartbeatFrequencyMS: this.minHeartbeatFrequencyMS,663      currentTime,664      timeSinceLastCall665    };666  }667 668  private _reschedule(ms?: number) {669    if (this.stopped) return;670    if (this.timerId) {671      clearTimeout(this.timerId);672    }673 674    this.timerId = setTimeout(this._executeAndReschedule, ms || this.heartbeatFrequencyMS);675  }676 677  private _executeAndReschedule = () => {678    if (this.stopped) return;679    if (this.timerId) {680      clearTimeout(this.timerId);681    }682 683    this.isExpeditedCallToFnScheduled = false;684    this.isExecutionInProgress = true;685 686    this.fn(() => {687      this.lastExecutionEnded = now();688      this.isExecutionInProgress = false;689      this._reschedule(this.heartbeatFrequencyMS);690    });691  };692}693 694/** @internal695 * This class implements the RTT sampling logic specified for [CSOT](https://github.com/mongodb/specifications/blob/bbb335e60cd7ea1e0f7cd9a9443cb95fc9d3b64d/source/client-side-operations-timeout/client-side-operations-timeout.md#drivers-use-minimum-rtt-to-short-circuit-operations)696 *697 * This is implemented as a [circular buffer](https://en.wikipedia.org/wiki/Circular_buffer) keeping698 * the most recent `windowSize` samples699 * */700export class RTTSampler {701  /** Index of the next slot to be overwritten */702  private writeIndex: number;703  private length: number;704  private rttSamples: Float64Array;705 706  constructor(windowSize = 10) {707    this.rttSamples = new Float64Array(windowSize);708    this.length = 0;709    this.writeIndex = 0;710  }711 712  /**713   * Adds an rtt sample to the end of the circular buffer714   * When `windowSize` samples have been collected, `addSample` overwrites the least recently added715   * sample716   */717  addSample(sample: number) {718    this.rttSamples[this.writeIndex++] = sample;719    if (this.length < this.rttSamples.length) {720      this.length++;721    }722 723    this.writeIndex %= this.rttSamples.length;724  }725 726  /**727   * When \< 2 samples have been collected, returns 0728   * Otherwise computes the minimum value samples contained in the buffer729   */730  min(): number {731    if (this.length < 2) return 0;732    let min = this.rttSamples[0];733    for (let i = 1; i < this.length; i++) {734      if (this.rttSamples[i] < min) min = this.rttSamples[i];735    }736 737    return min;738  }739 740  /**741   * Returns mean of samples contained in the buffer742   */743  average(): number {744    if (this.length === 0) return 0;745    let sum = 0;746    for (let i = 0; i < this.length; i++) {747      sum += this.rttSamples[i];748    }749 750    return sum / this.length;751  }752 753  /**754   * Returns most recently inserted element in the buffer755   * Returns null if the buffer is empty756   * */757  get last(): number | null {758    if (this.length === 0) return null;759    return this.rttSamples[this.writeIndex === 0 ? this.length - 1 : this.writeIndex - 1];760  }761 762  /**763   * Clear the buffer764   * NOTE: this does not overwrite the data held in the internal array, just the pointers into765   * this array766   */767  clear() {768    this.length = 0;769    this.writeIndex = 0;770  }771}772