mirror of
https://github.com/bvanroll/yahoo-thing.git
synced 2025-08-29 12:02:48 +00:00
412 lines
14 KiB
JavaScript
412 lines
14 KiB
JavaScript
'use strict';
|
|
const EventEmitter = require('events');
|
|
const MongoError = require('../error').MongoError;
|
|
const Pool = require('../connection/pool');
|
|
const relayEvents = require('../utils').relayEvents;
|
|
const calculateDurationInMs = require('../utils').calculateDurationInMs;
|
|
const Query = require('../connection/commands').Query;
|
|
const TwoSixWireProtocolSupport = require('../wireprotocol/2_6_support');
|
|
const ThreeTwoWireProtocolSupport = require('../wireprotocol/3_2_support');
|
|
const BSON = require('../connection/utils').retrieveBSON();
|
|
const createClientInfo = require('../topologies/shared').createClientInfo;
|
|
const Logger = require('../connection/logger');
|
|
const ServerDescription = require('./server_description').ServerDescription;
|
|
const ReadPreference = require('../topologies/read_preference');
|
|
const monitorServer = require('./monitoring').monitorServer;
|
|
|
|
/**
|
|
*
|
|
* @fires Server#serverHeartbeatStarted
|
|
* @fires Server#serverHeartbeatSucceeded
|
|
* @fires Server#serverHeartbeatFailed
|
|
*/
|
|
class Server extends EventEmitter {
|
|
/**
|
|
* Create a server
|
|
*
|
|
* @param {ServerDescription} description
|
|
* @param {Object} options
|
|
*/
|
|
constructor(description, options) {
|
|
super();
|
|
|
|
this.s = {
|
|
// the server description
|
|
description,
|
|
// a saved copy of the incoming options
|
|
options,
|
|
// the server logger
|
|
logger: Logger('Server', options),
|
|
// the bson parser
|
|
bson: options.bson || new BSON(),
|
|
// client metadata for the initial handshake
|
|
clientInfo: createClientInfo(options),
|
|
// state variable to determine if there is an active server check in progress
|
|
monitoring: false,
|
|
// the connection pool
|
|
pool: null
|
|
};
|
|
}
|
|
|
|
get description() {
|
|
return this.s.description;
|
|
}
|
|
|
|
get name() {
|
|
return this.s.description.address;
|
|
}
|
|
|
|
/**
|
|
* Initiate server connect
|
|
*
|
|
* @param {Array} [options.auth] Array of auth options to apply on connect
|
|
*/
|
|
connect(options) {
|
|
options = options || {};
|
|
|
|
// do not allow connect to be called on anything that's not disconnected
|
|
if (this.s.pool && !this.s.pool.isDisconnected() && !this.s.pool.isDestroyed()) {
|
|
throw new MongoError(`Server instance in invalid state ${this.s.pool.state}`);
|
|
}
|
|
|
|
// create a pool
|
|
this.s.pool = new Pool(this, Object.assign(this.s.options, options, { bson: this.s.bson }));
|
|
|
|
// Set up listeners
|
|
this.s.pool.on('connect', connectEventHandler(this));
|
|
this.s.pool.on('close', closeEventHandler(this));
|
|
|
|
// this.s.pool.on('error', errorEventHandler(this));
|
|
// this.s.pool.on('timeout', timeoutEventHandler(this));
|
|
// this.s.pool.on('parseError', errorEventHandler(this));
|
|
// this.s.pool.on('reconnect', reconnectEventHandler(this));
|
|
// this.s.pool.on('reconnectFailed', errorEventHandler(this));
|
|
|
|
// relay all command monitoring events
|
|
relayEvents(this.s.pool, this, ['commandStarted', 'commandSucceeded', 'commandFailed']);
|
|
|
|
// If auth settings have been provided, use them
|
|
if (options.auth) {
|
|
this.s.pool.connect.apply(this.s.pool, options.auth);
|
|
return;
|
|
}
|
|
|
|
this.s.pool.connect();
|
|
}
|
|
|
|
/**
|
|
* Destroy the server connection
|
|
*
|
|
* @param {Boolean} [options.emitClose=false] Emit close event on destroy
|
|
* @param {Boolean} [options.emitDestroy=false] Emit destroy event on destroy
|
|
* @param {Boolean} [options.force=false] Force destroy the pool
|
|
*/
|
|
destroy(callback) {
|
|
if (typeof callback === 'function') {
|
|
callback(null, null);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Immediately schedule monitoring of this server. If there already an attempt being made
|
|
* this will be a no-op.
|
|
*/
|
|
monitor() {
|
|
if (this.s.monitoring) return;
|
|
if (this.s.monitorId) clearTimeout(this.s.monitorId);
|
|
monitorServer(this);
|
|
}
|
|
|
|
/**
|
|
* Execute a command
|
|
*
|
|
* @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
|
|
* @param {object} cmd The command hash
|
|
* @param {ReadPreference} [options.readPreference] Specify read preference if command supports it
|
|
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized.
|
|
* @param {Boolean} [options.checkKeys=false] Specify if the bson parser should validate keys.
|
|
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
|
|
* @param {Boolean} [options.fullResult=false] Return the full envelope instead of just the result document.
|
|
* @param {ClientSession} [options.session=null] Session to use for the operation
|
|
* @param {opResultCallback} callback A callback function
|
|
*/
|
|
command(ns, cmd, options, callback) {
|
|
if (typeof options === 'function') {
|
|
(callback = options), (options = {}), (options = options || {});
|
|
}
|
|
|
|
const error = basicReadValidations(this, options);
|
|
if (error) {
|
|
return callback(error, null);
|
|
}
|
|
|
|
// Clone the options
|
|
options = Object.assign({}, options, { wireProtocolCommand: false });
|
|
|
|
// Debug log
|
|
if (this.s.logger.isDebug()) {
|
|
this.s.logger.debug(
|
|
`executing command [${JSON.stringify({ ns, cmd, options })}] against ${this.name}`
|
|
);
|
|
}
|
|
|
|
// Check if we have collation support
|
|
if (this.description.maxWireVersion < 5 && cmd.collation) {
|
|
callback(new MongoError(`server ${this.name} does not support collation`));
|
|
return;
|
|
}
|
|
|
|
// Create the query object
|
|
const query = this.s.wireProtocolHandler.command(this, ns, cmd, {}, options);
|
|
// Set slave OK of the query
|
|
query.slaveOk = options.readPreference ? options.readPreference.slaveOk() : false;
|
|
|
|
// write options
|
|
const writeOptions = {
|
|
raw: typeof options.raw === 'boolean' ? options.raw : false,
|
|
promoteLongs: typeof options.promoteLongs === 'boolean' ? options.promoteLongs : true,
|
|
promoteValues: typeof options.promoteValues === 'boolean' ? options.promoteValues : true,
|
|
promoteBuffers: typeof options.promoteBuffers === 'boolean' ? options.promoteBuffers : false,
|
|
command: true,
|
|
monitoring: typeof options.monitoring === 'boolean' ? options.monitoring : false,
|
|
fullResult: typeof options.fullResult === 'boolean' ? options.fullResult : false,
|
|
requestId: query.requestId,
|
|
socketTimeout: typeof options.socketTimeout === 'number' ? options.socketTimeout : null,
|
|
session: options.session || null
|
|
};
|
|
|
|
// write the operation to the pool
|
|
this.s.pool.write(query, writeOptions, callback);
|
|
}
|
|
|
|
/**
|
|
* Insert one or more documents
|
|
* @method
|
|
* @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
|
|
* @param {array} ops An array of documents to insert
|
|
* @param {boolean} [options.ordered=true] Execute in order or out of order
|
|
* @param {object} [options.writeConcern={}] Write concern for the operation
|
|
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized.
|
|
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
|
|
* @param {ClientSession} [options.session=null] Session to use for the operation
|
|
* @param {opResultCallback} callback A callback function
|
|
*/
|
|
insert(ns, ops, options, callback) {
|
|
executeWriteOperation({ server: this, op: 'insert', ns, ops }, options, callback);
|
|
}
|
|
|
|
/**
|
|
* Perform one or more update operations
|
|
* @method
|
|
* @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
|
|
* @param {array} ops An array of updates
|
|
* @param {boolean} [options.ordered=true] Execute in order or out of order
|
|
* @param {object} [options.writeConcern={}] Write concern for the operation
|
|
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized.
|
|
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
|
|
* @param {ClientSession} [options.session=null] Session to use for the operation
|
|
* @param {opResultCallback} callback A callback function
|
|
*/
|
|
update(ns, ops, options, callback) {
|
|
executeWriteOperation({ server: this, op: 'update', ns, ops }, options, callback);
|
|
}
|
|
|
|
/**
|
|
* Perform one or more remove operations
|
|
* @method
|
|
* @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
|
|
* @param {array} ops An array of removes
|
|
* @param {boolean} [options.ordered=true] Execute in order or out of order
|
|
* @param {object} [options.writeConcern={}] Write concern for the operation
|
|
* @param {Boolean} [options.serializeFunctions=false] Specify if functions on an object should be serialized.
|
|
* @param {Boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
|
|
* @param {ClientSession} [options.session=null] Session to use for the operation
|
|
* @param {opResultCallback} callback A callback function
|
|
*/
|
|
remove(ns, ops, options, callback) {
|
|
executeWriteOperation({ server: this, op: 'remove', ns, ops }, options, callback);
|
|
}
|
|
}
|
|
|
|
function basicWriteValidations(server) {
|
|
if (!server.s.pool) {
|
|
return new MongoError('server instance is not connected');
|
|
}
|
|
|
|
if (server.s.pool.isDestroyed()) {
|
|
return new MongoError('server instance pool was destroyed');
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function basicReadValidations(server, options) {
|
|
const error = basicWriteValidations(server, options);
|
|
if (error) {
|
|
return error;
|
|
}
|
|
|
|
if (options.readPreference && !(options.readPreference instanceof ReadPreference)) {
|
|
return new MongoError('readPreference must be an instance of ReadPreference');
|
|
}
|
|
}
|
|
|
|
function executeWriteOperation(args, options, callback) {
|
|
if (typeof options === 'function') (callback = options), (options = {});
|
|
options = options || {};
|
|
|
|
// TODO: once we drop Node 4, use destructuring either here or in arguments.
|
|
const server = args.server;
|
|
const op = args.op;
|
|
const ns = args.ns;
|
|
const ops = Array.isArray(args.ops) ? args.ops : [args.ops];
|
|
|
|
const error = basicWriteValidations(server, options);
|
|
if (error) {
|
|
callback(error, null);
|
|
return;
|
|
}
|
|
|
|
// Check if we have collation support
|
|
if (server.description.maxWireVersion < 5 && options.collation) {
|
|
callback(new MongoError(`server ${this.name} does not support collation`));
|
|
return;
|
|
}
|
|
|
|
// Execute write
|
|
return server.s.wireProtocolHandler[op](server.s.pool, ns, server.s.bson, ops, options, callback);
|
|
}
|
|
|
|
function saslSupportedMechs(options) {
|
|
if (!options) {
|
|
return {};
|
|
}
|
|
|
|
const authArray = options.auth || [];
|
|
const authMechanism = authArray[0] || options.authMechanism;
|
|
const authSource = authArray[1] || options.authSource || options.dbName || 'admin';
|
|
const user = authArray[2] || options.user;
|
|
|
|
if (typeof authMechanism === 'string' && authMechanism.toUpperCase() !== 'DEFAULT') {
|
|
return {};
|
|
}
|
|
|
|
if (!user) {
|
|
return {};
|
|
}
|
|
|
|
return { saslSupportedMechs: `${authSource}.${user}` };
|
|
}
|
|
|
|
function extractIsMasterError(err, result) {
|
|
if (err) return err;
|
|
if (result && result.result && result.result.ok === 0) {
|
|
return new MongoError(result.result);
|
|
}
|
|
}
|
|
|
|
function executeServerHandshake(server, callback) {
|
|
// construct an `ismaster` query
|
|
const compressors =
|
|
server.s.options.compression && server.s.options.compression.compressors
|
|
? server.s.options.compression.compressors
|
|
: [];
|
|
|
|
const queryOptions = { numberToSkip: 0, numberToReturn: -1, checkKeys: false, slaveOk: true };
|
|
const query = new Query(
|
|
server.s.bson,
|
|
'admin.$cmd',
|
|
Object.assign(
|
|
{ ismaster: true, client: server.s.clientInfo, compression: compressors },
|
|
saslSupportedMechs(server.s.options)
|
|
),
|
|
queryOptions
|
|
);
|
|
|
|
// execute the query
|
|
server.s.pool.write(
|
|
query,
|
|
{ socketTimeout: server.s.options.connectionTimeout || 2000 },
|
|
callback
|
|
);
|
|
}
|
|
|
|
function configureWireProtocolHandler(ismaster) {
|
|
// 3.2 wire protocol handler
|
|
if (ismaster.maxWireVersion >= 4) {
|
|
return new ThreeTwoWireProtocolSupport();
|
|
}
|
|
|
|
// default to 2.6 wire protocol handler
|
|
return new TwoSixWireProtocolSupport();
|
|
}
|
|
|
|
function connectEventHandler(server) {
|
|
return function() {
|
|
// log information of received information if in info mode
|
|
// if (server.s.logger.isInfo()) {
|
|
// var object = err instanceof MongoError ? JSON.stringify(err) : {};
|
|
// server.s.logger.info(`server ${server.name} fired event ${event} out with message ${object}`);
|
|
// }
|
|
|
|
// begin initial server handshake
|
|
const start = process.hrtime();
|
|
executeServerHandshake(server, (err, response) => {
|
|
// Set initial lastIsMasterMS - is this needed?
|
|
server.s.lastIsMasterMS = calculateDurationInMs(start);
|
|
|
|
const serverError = extractIsMasterError(err, response);
|
|
if (serverError) {
|
|
server.emit('error', serverError);
|
|
return;
|
|
}
|
|
|
|
// extract the ismaster from the server response
|
|
const isMaster = response.result;
|
|
|
|
// compression negotation
|
|
if (isMaster && isMaster.compression) {
|
|
const localCompressionInfo = server.s.options.compression;
|
|
const localCompressors = localCompressionInfo.compressors;
|
|
for (var i = 0; i < localCompressors.length; i++) {
|
|
if (isMaster.compression.indexOf(localCompressors[i]) > -1) {
|
|
server.s.pool.options.agreedCompressor = localCompressors[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (localCompressionInfo.zlibCompressionLevel) {
|
|
server.s.pool.options.zlibCompressionLevel = localCompressionInfo.zlibCompressionLevel;
|
|
}
|
|
}
|
|
|
|
// configure the wire protocol handler
|
|
server.s.wireProtocolHandler = configureWireProtocolHandler(isMaster);
|
|
|
|
// log the connection event if requested
|
|
if (server.s.logger.isInfo()) {
|
|
server.s.logger.info(
|
|
`server ${server.name} connected with ismaster [${JSON.stringify(isMaster)}]`
|
|
);
|
|
}
|
|
|
|
// emit an event indicating that our description has changed
|
|
server.emit(
|
|
'descriptionReceived',
|
|
new ServerDescription(server.description.address, isMaster)
|
|
);
|
|
|
|
// emit a connect event
|
|
server.emit('connect', isMaster);
|
|
});
|
|
};
|
|
}
|
|
|
|
function closeEventHandler(server) {
|
|
return function() {
|
|
server.emit('close');
|
|
};
|
|
}
|
|
|
|
module.exports = Server;
|