// *****************************************************************************
// Copyright 2013-2023 Aerospike, Inc.
//
// Licensed 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 policy = require('./policy')
const inspect = Symbol.for('nodejs.util.inspect.custom')
class Config {
/**
* @class Config
* @classdesc config command.
* The Config class contains the settings for an Aerospike client
* instance, including the list of seed hosts, default policies, and other
* settings.
*
* @type Class
* @throws {TypeError} If invalid config values are passed.
*
* @example
*
* const Aerospike = require('aerospike')
*
* let config = {
* hosts: '192.168.1.10,192.168.1.11',
* user: process.env.DATABASE_USER,
* password: process.env.DATABASE_PASSWORD,
* policies: {
* read: new Aerospike.ReadPolicy({
* totalTimeout: 0
* })
* },
* log: {
* level: Aerospike.log.INFO,
* file: 2 // log to stderr
* }
* }
*
* Aerospike.connect(config)
* .then(client => {
* // client is ready to accept commands
* client.close()
* })
* .catch(error => {
* console.error('Failed to connect to cluster: %s', error.message)
* })
*
*
* // Initializes a new client configuration from the given config values.
*
* @param {Object} [config] configuration values
*/
constructor (config) {
config = config || {}
/**
* @name Config#user
* @summary The user name to use when authenticating to the cluster.
* @description Leave empty for clusters running without access management.
* (Security features are available in the Aerospike Database Enterprise
* Edition.)
* @type {string}
*/
if (typeof config.user === 'string') {
this.user = config.user
}
/**
* @name Config#password
* @summary The password to use when authenticating to the cluster.
* @type {string}
*/
if (typeof config.password === 'string') {
this.password = config.password
}
/**
* @name Config#authMode
* @summary Authentication mode used when user/password is defined.
* @description One of the auth modes defined in {@link module:aerospike.auth}.
* @type {number}
* @see module:aerospike.auth
*/
if (typeof config.authMode === 'number') {
this.authMode = config.authMode
}
/**
* @name Config#clusterName
* @summary Expected Cluster Name.
* @description If not <code>null</code>, server nodes must return this
* cluster name in order to join the client's view of the cluster. Should
* only be set when connecting to servers that support the "cluster-name"
* info command.
* @type {string}
* @since v2.4
*/
this.clusterName = config.clusterName
/**
* @name Config#port
* @summary Default port to use for any host address, that does not
* explicitly specify a port number. Default is 3000.
* @type {number}
*
* @since v2.4
*/
if (typeof config.port === 'number') {
this.port = config.port
} else {
this.port = 3000
}
/**
* @name Config#tls
* @summary Configure Transport Layer Security (TLS) parameters for secure
* connections to the database cluster. TLS connections are not supported as
* of Aerospike Server v3.9 and depend on a future server release.
* @type {Object}
* @since v2.4
*
* @property {boolean} [enable=true] - Enable TLS for socket connections to
* cluster nodes. By default TLS is enabled only if the client configuration
* includes a <code>tls</code> section.
* @property {string} [cafile] - Path to a trusted CA certificate file. By
* default TLS will use system standard trusted CA certificates.
* @property {string} [capath] - Path to a directory of trusted certificates.
* See the OpenSSL SSL_CTX_load_verify_locations manual page for more
* information about the format of the directory.
* @property {string} [protocols] - Specifies enabled protocols. The format is
* the same as Apache's SSLProtocol documented at
* https://httpd.apache.org/docs/current/mod/mod_ssl.html#sslprotocol. If not
* specified, the client will use "-all +TLSv1.2". If you are not sure what
* protocols to select this option is best left unspecified.
* @property {string} [cipherSuite] - Specifies enabled cipher suites. The
* format is the same as OpenSSL's Cipher List Format documented at
* https://www.openssl.org/docs/manmaster/apps/ciphers.html. If not specified
* the OpenSSL default cipher suite described in the ciphers documentation
* will be used. If you are not sure what cipher suite to select this option
* is best left unspecified.
* @property {string} [certBlacklist] - Path to a certificate blacklist file.
* The file should contain one line for each blacklisted certificate. Each
* line starts with the certificate serial number expressed in hex. Each
* entry may optionally specify the issuer name of the certificate. (Serial
* numbers are only required to be unique per issuer.) Example records:
* <code><br>867EC87482B2 /C=US/ST=CA/O=Acme/OU=Engineering/CN=Test Chain CA<br>
* E2D4B0E570F9EF8E885C065899886461</code>
* @property {string} [keyfile] - Path to the client's key for mutual
* authentication. By default, mutual authentication is disabled.
* @property {string} [keyfilePassword] - Decryption password for the
* client's key for mutual authentication. By default, the key is assumed
* not to be encrypted.
* @property {string} [certfile] - Path to the client's certificate chain
* file for mutual authentication. By default, mutual authentication is
* disabled.
* @property {boolean} [crlCheck=false] - Enable CRL checking for the
* certificate chain leaf certificate. An error occurs if a suitable CRL
* cannot be found. By default CRL checking is disabled.
* @property {boolean} [crlCheckAll=false] - Enable CRL checking for the
* entire certificate chain. An error occurs if a suitable CRL cannot be
* found. By default CRL checking is disabled.
* @property {boolean} [logSessionInfo=false] - Log session information for
* each connection.
* @property {boolean} [forLoginOnly=false] - Use TLS connections only for login
* authentication. All other communication with the server will be done
* with non-TLS connections. Default: false (Use TLS connections for all
* communication with the server.)
*/
if (typeof config.tls === 'object') {
this.tls = config.tls
}
/**
* @summary List of hosts with which the client should attempt to connect.
* @description If not specified, the client attempts to read the host list
* from the <code>AEROSPIKE_HOSTS</code> environment variable or else falls
* back to use a default value of "localhost".
* @type {(Host[] | string)}
*
* @example <caption>Setting <code>hosts</code> using a string:</caption>
*
* const Aerospike = require('aerospike')
*
* const hosts = '192.168.0.1:3000,192.168.0.2:3000'
* const client = await Aerospike.connect({ hosts })
*
* @example <caption>Setting <code>hosts</code> using an array of hostname/port tuples:</caption>
*
* const Aerospike = require('aerospike')
*
* const hosts = [
* { addr: '192.168.0.1', port: 3000 },
* { addr: '192.168.0.2', port: 3000 }
* ]
* const client = await Aerospike.connect({ hosts })
*
* @example <caption>Setting <code>hosts</code> with TLS name using a string:</caption>
*
* const Aerospike = require('aerospike')
*
* const hosts = '192.168.0.1:example.com:3000,192.168.0.2:example.com:3000'
* const client = await Aerospike.connect({ hosts })
*
* @example <caption>Setting <code>hosts</code> using an array of hostname/port/tlsname tuples:</caption>
*
* const Aerospike = require('aerospike')
*
* const hosts = [
* { addr: '192.168.0.1', port: 3000, tlsname: 'example.com' },
* { addr: '192.168.0.2', port: 3000, tlsname: 'example.com' }
* ]
* const client = await Aerospike.connect({ hosts })
*/
this.hosts = config.hosts || process.env.AEROSPIKE_HOSTS || `localhost:${this.port}`
/**
* @summary Global client policies.
*
* @description The configuration defines default policies for the
* application. Policies define the behavior of the client, which can be
* global for all uses of a single type of operation, or local to a single
* use of an operation.
*
* Each database operation accepts a policy for that operation as an
* argument. This is considered a local policy, and is a single use policy.
* This local policy supersedes any global policy defined.
*
* If a value of the policy is not defined, then the rule is to fallback to
* the global policy for that operation. If the global policy for that
* operation is undefined, then the global default value will be used.
*
* If you find that you have behavior that you want every use of an
* operation to utilize, then you can specify the default policy as
* {@link Config#policies}.
*
* For example, the {@link Client#put} operation takes a {@link
* WritePolicy} parameter. If you find yourself setting the {@link
* WritePolicy#key} policy value for every call to {@link Client.put}, then
* you may find it beneficial to set the global {@link WritePolicy} in
* {@link Config#policies}, which all operations will use.
*
* @type {Policies}
*
* @example <caption>Setting a default <code>key</code> policy for all write operations</caption>
*
* const Aerospike = require('aerospike')
*
* // INSERT HOSTNAME AND PORT NUMBER OF AEROSPIKE SERVER NODE HERE!
* var config = {
* hosts: '192.168.33.10:3000',
* policies: {
* write: new Aerospike.WritePolicy({
* key: Aerospike.policy.key.SEND,
* socketTimeout : 0,
* totalTimeout : 0
* })
* }
* }
*
* let key = new Aerospike.Key('test', 'demo', 123)
*
* Aerospike.connect(config)
* .then(client => {
* return client.put(key, {int: 42})
* .then(() => client.close())
* .catch(error => {
* throw error
* client.close()
* })
* })
* .catch(console.error)
*/
this.policies = {}
if (typeof config.policies === 'object') {
this.setDefaultPolicies(config.policies)
}
/**
* @name Config#log
* @summary Configuration for logging done by the client.
* @type {Object}
*
* @property {Number} [log.level] - Log level; see {@link
* module:aerospike.log} for details.
* @property {Number} [log.file] - File descriptor returned by
* <code>fs.open()</code> or one of <code>process.stdout.fd</code> or
* <code>process.stderr.fd</code>.
*
* @example <caption>Enabling debug logging to a separate log file</caption>
*
* const Aerospike = require('aerospike')
*
* const fs = require('fs')
*
* var debuglog = fs.openSync('./debug.log', 'w')
* // INSERT HOSTNAME AND PORT NUMBER OF AEROSPIKE SERVER NODE HERE!
* var config = {
* hosts: '192.168.33.10:3000',
* log: {
* level: Aerospike.log.DEBUG,
* file: debuglog
* }
* }
* Aerospike.connect(config, (err, client) => {
* if (err) throw err
* console.log("Connected. Now closing connection.")
* client.close()
* })
*/
if (typeof config.log === 'object') {
this.log = config.log
}
/**
* @name Config#connTimeoutMs
* @summary Initial host connection timeout in milliseconds.
* @description The client observes this timeout when opening a connection to
* the cluster for the first time.
* @type {number}
* @default 1000
*/
if (Number.isInteger(config.connTimeoutMs)) {
this.connTimeoutMs = config.connTimeoutMs
}
/**
* @name Config#loginTimeoutMs
* @summary Node login timeout in milliseconds.
* @type {number}
* @default 5000
*/
if (Number.isInteger(config.loginTimeoutMs)) {
this.loginTimeoutMs = config.loginTimeoutMs
}
/**
* @name Config#maxSocketIdle
*
* @summary Maximum socket idle time in seconds.
*
* @description Connection pools will discard sockets that have been idle
* longer than the maximum. The value is limited to 24 hours (86400).
*
* It's important to set this value to a few seconds less than the server's
* <code>proto-fd-idle-ms</code> (default 60000 milliseconds or 1 minute),
* so the client does not attempt to use a socket that has already been
* reaped by the server.
*
* Connection pools are now implemented by a LIFO stack. Connections at the
* tail of the stack will always be the least used. These connections are
* checked for <code>maxSocketIdle</code> once every 30 tend iterations
* (usually 30 seconds).
*
* @type {number}
*
* @default 0 seconds
*/
if (Number.isInteger(config.maxSocketIdle)) {
this.maxSocketIdle = config.maxSocketIdle
}
/**
* @name Config#tenderInterval
* @summary Polling interval in milliseconds for cluster tender.
* @type {number}
* @default 1000
*/
if (Number.isInteger(config.tenderInterval)) {
this.tenderInterval = config.tenderInterval
}
/**
* @name Config#maxConnsPerNode
*
* @summary Maximum number of asynchronous connections allowed per server node.
*
* @description New transactions will be rejected with an {@link
* module:aerospike/status.ERR_NO_MORE_CONNECTIONS|ERR_NO_MORE_CONNECTIONS}
* error if the limit would be exceeded.
*
* @type {number}
*
* @default 100
*/
if (Number.isInteger(config.maxConnsPerNode)) {
this.maxConnsPerNode = config.maxConnsPerNode
}
/**
* @name Config#maxErrorRate
*
* @summary Maximum number of errors allowed per node per error_rate_window before backoff algorithm returns
* AEROSPIKE_MAX_ERROR_RATE for database commands to that node. If max_error_rate is zero, there is no error limit.
* The counted error types are any error that causes the connection to close (socket errors and client timeouts),
* server device overload and server timeouts.
*
* The application should backoff or reduce the transaction load until AEROSPIKE_MAX_ERROR_RATE stops being returned.
*
* @description If the backoff algorithm has been activated, transactions will fail with {@link
* module:aerospike/status.AEROSPIKE_MAX_ERROR_RATE|AEROSPIKE_MAX_ERROR_RATE} until the {@link Config#errorRateWindow} has passed and the
* error count has been reset.
*
* @type {number}
*
* @default 100
*/
if (Number.isInteger(config.maxErrorRate)) {
this.maxErrorRate = config.maxErrorRate
}
/**
* @name Config#errorRateWindow
*
* @summary The number of cluster tend iterations that defines the window for {@link Config#maxErrorRate} to be surpassed. One tend iteration is defined
* as {@link Config#tendInterval} plus the time to tend all nodes. At the end of the window, the error count is reset to zero and backoff state is removed on all nodes.
*
* @type {number}
*
* @default 1
*/
if (Number.isInteger(config.errorRateWindow)) {
this.errorRateWindow = config.errorRateWindow
}
/**
* @name Config#minConnsPerNode
*
* @summary Minimum number of asynchronous connections allowed per server node.
*
* @description Preallocate min connections on client node creation. The
* client will periodically allocate new connections if count falls below
* min connections.
*
* Server <code>proto-fd-idle-ms</code> may also need to be increased
* substantially if min connections are defined. The
* <code>proto-fd-idle-ms</code> default directs the server to close
* connections that are idle for 60 seconds which can defeat the purpose of
* keeping connections in reserve for a future burst of activity.
*
* If server <code>proto-fd-idle-ms</code> is changed, client {@link
* Config#maxSocketIdle} should also be changed to be a few seconds less
* than <code>proto-fd-idle-ms</code>.
*
* @type {number}
* @default 0
*/
if (Number.isInteger(config.minConnsPerNode)) {
this.minConnsPerNode = config.minConnsPerNode
}
if (typeof config.modlua === 'object') {
/**
* @summary Configuration values for the mod-lua user path.
* @description If you are using user-defined functions (UDF) for processing
* query results (i.e. aggregations), then you will find it useful to set
* the <code>modlua</code> settings. Of particular importance is the
* <code>modelua.userPath</code>, which allows you to define a path to where
* the client library will look for Lua files for processing.
* @type {Object}
*
* @property {string} [modlua.userPath] - Path to user Lua scripts.
*/
this.modlua = Object.assign({}, config.modlua)
}
/**
* @name Config#sharedMemory
* @summary Shared memory configuration.
* @description This allows multiple client instances running in separate
* processes on the same machine to share cluster status, including nodes and
* data partition maps. Each shared memory segment contains state for one
* Aerospike cluster. If there are multiple Aerospike clusters, a different
* <code>key</code> must be defined for each cluster.
* @type {Object}
* @see {@link http://www.aerospike.com/docs/client/c/usage/shm.html#operational-notes|Operational Notes}
* @tutorial node_clusters
*
* @property {boolean} [enable=true] - Whether to enable/disable usage of
* shared memory.
* @property {number} key - Identifier for the shared memory segment
* associated with the target Aerospike cluster; the same key needs to be
* used on all client instances connecting to the same cluster.
* @property {number} [maxNodes=16] - Sets the max. number of
* server nodes in the cluster - this value is required to size the shared
* memory segment. Ensure that you leave a cushion between actual server node
* cound and <code>maxNodes</code> so that you can add new nodes without
* rebooting the client.
* @property {number} [maxNamespaces=8] - Sets the max. number of
* namespaces used in the cluster - this value is required to size the shared
* memory segment. Ensure that you leave a cushion between actual namespace
* count and <code>maxNamespaces</code> so that you can add new namespaces
* without rebooking the client.
* @property {number} [takeoverThresholdSeconds=30] - Expiration
* time in seconds for the lock on the shared memory segment; if the cluster
* status has not been updated after this many seconds another client instance
* will take over the shared memory cluster tending.
*
* @example <caption>Using shared memory in a clustered setup</caption>
*
* const Aerospike = require('aerospike')
* const cluster = require('cluster')
*
* const config = {
* sharedMemory: {
* key: 0xa5000000
* }
* }
* const client = Aerospike.client(config)
* const noWorkers = 4
*
* if (cluster.isMaster) {
* // spawn new worker processes
* for (var i = 0; i < noWorkers; i++) {
* cluster.fork()
* }
* } else {
* // connect to Aerospike cluster in each worker process
* client.connect((err) => { if (err) throw err })
*
* // handle incoming HTTP requests, etc.
* // http.createServer((request, response) => { ... })
*
* // close DB connection on shutdown
* client.close()
* }
*/
if (typeof config.sharedMemory === 'object') {
this.sharedMemory = config.sharedMemory
}
/**
* @name Config#useAlternateAccessAddress
* @summary Whether the client should use the server's
* <code>alternate-access-address</code> instead of the
* <code>access-address</code>.
*
* @type {boolean}
* @default false
* @since v3.7.1
*/
this.useAlternateAccessAddress = Boolean(config.useAlternateAccessAddress)
/**
* @name Config#rackAware
* @summary Track server rack data.
* @description This field is useful when directing read commands to the
* server node that contains the key and exists on the same rack as the
* client. This serves to lower cloud provider costs when nodes are
* distributed across different racks/data centers.
*
* {@link Config#rackId rackId} config, {@link
* module:aerospike/policy.replica PREFER_RACK} replica policy, and server
* rack configuration must also be set to enable this functionality.
*
* @type {boolean}
* @default false
* @since 3.8.0
*/
this.rackAware = config.rackAware
/**
* @name Config#rackId
* @summary Rack where this client instance resides.
* @description {@link Config#rackAware rackAware} config, {@link
* module:aerospike/policy.replica PREFER_RACK} replica policy, and server
* rack configuration must also be set to enable this functionality.
*
* @type {number}
* @default 0
* @since 3.8.0
*/
if (Number.isInteger(config.rackId)) {
this.rackId = config.rackId
}
}
/**
* Set default policies from the given policy values.
*
* @param {Policies} one or more default policies
* @throws {TypeError} if any of the properties of the policies object is not
* a valid policy type
*/
setDefaultPolicies (policies) {
for (const type in policies) {
const values = policies[type]
this.policies[type] = policy.createPolicy(type, values)
}
}
/**
* Custom inspector that masks the password property when printing the
* config.
*
* @private
*/
[inspect] () {
const copy = Object.assign({}, this)
if (this.password !== undefined) {
Object.assign(copy, { password: '[FILTERED]' })
}
return copy
}
}
module.exports = Config