UNPKG

@opensearch-project/opensearch

Version:

The official OpenSearch client for Node.js

287 lines (251 loc) 8.13 kB
/* * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * */ /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ 'use strict'; const { URL } = require('url'); const debug = require('debug')('opensearch'); const Connection = require('../Connection'); const { ConfigurationError } = require('../errors'); const noop = () => {}; class BaseConnectionPool { constructor(opts) { // list of nodes and weights this.connections = []; // how many nodes we have in our scheduler this.size = this.connections.length; this.Connection = opts.Connection; this.emit = opts.emit || noop; this.auth = opts.auth || null; this._ssl = opts.ssl; this._agent = opts.agent; this._proxy = opts.proxy || null; } getConnection() { throw new Error('getConnection must be implemented'); } markAlive() { return this; } markDead() { return this; } /** * Creates a new connection instance. */ createConnection(opts) { if (opts instanceof Connection) { throw new ConfigurationError('The argument provided is already a Connection instance.'); } if (typeof opts === 'string') { opts = this.urlToHost(opts); } if (this.auth !== null) { opts.auth = this.auth; } else if (opts.url.username !== '' && opts.url.password !== '') { opts.auth = { username: decodeURIComponent(opts.url.username), password: decodeURIComponent(opts.url.password), }; } if (opts.ssl == null) opts.ssl = this._ssl; /* istanbul ignore else */ if (opts.agent == null) opts.agent = this._agent; /* istanbul ignore else */ if (opts.proxy == null) opts.proxy = this._proxy; const connection = new this.Connection(opts); for (const conn of this.connections) { if (conn.id === connection.id) { throw new Error(`Connection with id '${connection.id}' is already present`); } } return connection; } /** * Adds a new connection to the pool. * * @param {object|string} host * @returns {ConnectionPool} */ addConnection(opts) { if (Array.isArray(opts)) { opts.forEach((o) => this.addConnection(o)); return; } if (typeof opts === 'string') { opts = this.urlToHost(opts); } const connectionId = opts.id; const connectionUrl = opts.url.href; if (connectionId || connectionUrl) { const connectionById = this.connections.find((c) => c.id === connectionId); const connectionByUrl = this.connections.find((c) => c.id === connectionUrl); if (connectionById || connectionByUrl) { throw new ConfigurationError( `Connection with id '${connectionId || connectionUrl}' is already present` ); } } this.update([...this.connections, opts]); return this.connections[this.size - 1]; } /** * Removes a new connection to the pool. * * @param {object} connection * @returns {ConnectionPool} */ removeConnection(connection) { debug('Removing connection', connection); return this.update(this.connections.filter((c) => c.id !== connection.id)); } /** * Empties the connection pool. */ empty(callback = noop) { debug('Emptying the connection pool'); let openConnections = this.size; this.connections.forEach((connection) => { connection.close(() => { if (--openConnections === 0) { this.connections = []; this.size = this.connections.length; callback(); } }); }); } /** * Update the ConnectionPool with new connections. * * @param {array} array of connections * @returns {ConnectionPool} */ update(nodes) { debug('Updating the connection pool'); const newConnections = []; const oldConnections = []; for (const node of nodes) { // if we already have a given connection in the pool // we mark it as alive and we do not close the connection // to avoid socket issues const connectionById = this.connections.find((c) => c.id === node.id); const connectionByUrl = this.connections.find((c) => c.id === node.url.href); if (connectionById) { debug(`The connection with id '${node.id}' is already present`); this.markAlive(connectionById); newConnections.push(connectionById); // in case the user has passed a single url (or an array of urls), // the connection id will be the full href; to avoid closing valid connections // because are not present in the pool, we check also the node url, // and if is already present we update its id with the opensearch provided one. } else if (connectionByUrl) { connectionByUrl.id = node.id; this.markAlive(connectionByUrl); newConnections.push(connectionByUrl); } else { newConnections.push(this.createConnection(node)); } } const ids = nodes.map((c) => c.id); // remove all the dead connections and old connections for (const connection of this.connections) { if (ids.indexOf(connection.id) === -1) { oldConnections.push(connection); } } // close old connections oldConnections.forEach((connection) => connection.close()); this.connections = newConnections; this.size = this.connections.length; return this; } /** * Transforms the nodes objects to a host object. * * @param {object} nodes * @returns {array} hosts */ nodesToHost(nodes, protocol) { const ids = Object.keys(nodes); const hosts = []; for (let i = 0, len = ids.length; i < len; i++) { const node = nodes[ids[i]]; // New nodes do not have the http property populated yet. Skip this for now. if (node.http === undefined) { continue; } // If there is no protocol in // the `publish_address` new URL will throw // the publish_address can have two forms: // - ip:port // - hostname/ip:port // if we encounter the second case, we should // use the hostname instead of the ip let address = node.http.publish_address; const parts = address.split('/'); // the url is in the form of hostname/ip:port if (parts.length > 1) { const hostname = parts[0]; const port = parts[1].match(/((?::))(?:[0-9]+)$/g)[0].slice(1); address = `${hostname}:${port}`; } address = address.slice(0, 4) === 'http' ? /* istanbul ignore next */ address : `${protocol}//${address}`; const roles = node.roles.reduce((acc, role) => { acc[role] = true; return acc; }, {}); hosts.push({ url: new URL(address), id: ids[i], roles: Object.assign( { [Connection.roles.DATA]: false, [Connection.roles.INGEST]: false, }, roles ), }); } return hosts; } /** * Transforms an url string to a host object * * @param {string} url * @returns {object} host */ urlToHost(url) { return { url: new URL(url), }; } } module.exports = BaseConnectionPool;