oracle-nosqldb
Version:
Node.js driver for Oracle NoSQL Database
406 lines (378 loc) • 14.5 kB
JavaScript
/*-
* Copyright (c) 2018, 2024 Oracle and/or its affiliates. All rights reserved.
*
* Licensed under the Universal Permissive License v 1.0 as shown at
* https://oss.oracle.com/licenses/upl/
*/
;
const assert = require('assert');
const ServiceType = require('./constants').ServiceType;
const Consistency = require('./constants').Consistency;
const ErrorCode = require('./error_code');
const NoSQLArgumentError = require('./error').NoSQLArgumentError;
const isPosInt32 = require('./utils').isPosInt32;
const isPosInt32OrZero = require('./utils').isPosInt32OrZero;
const isPlainObject = require('./utils').isPlainObject;
const requireNoWP = require('./utils').requireNoWP;
const path = require('path');
const AuthConfig = require('./auth/config');
const NumberTypeHandler = require('./db_number');
const Region = require('./region');
class Config {
//default retry.handler.doRetry()
static _shouldRetry(req, numRetries, err) {
assert(err);
switch(err.errorCode) {
case ErrorCode.OPERATION_LIMIT_EXCEEDED:
return req.opt.retry.controlOpBaseDelay &&
req.opt.timeout > req.opt.retry.controlOpBaseDelay;
case ErrorCode.SECURITY_INFO_UNAVAILABLE:
case ErrorCode.NETWORK_ERROR:
return true;
case ErrorCode.INVALID_AUTHORIZATION:
//Retry once in case the error is due to expired authorization. But
//if last error was the same, then the error is not due to expiration
//so we don't retry anymore.
return !req.lastError || req.lastError.errorCode !==
ErrorCode.INVALID_AUTHORIZATION;
default:
break;
}
assert(req._op);
if (!req._op.shouldRetry(req)) {
return false;
}
return numRetries < req.opt.retry.maxRetries;
}
static _backoffDelay(numRetries, baseDelay) {
let ms = (1 << (numRetries - 1)) * baseDelay;
ms += Math.floor(Math.random() * baseDelay);
return ms;
}
static _secInfoNotReadyDelay(numRetries, numBackoff, baseDelay) {
return numRetries > numBackoff ?
Config._backoffDelay(numRetries - numBackoff, baseDelay) :
baseDelay;
}
//default retry.handler.delay()
static _retryDelay(req, numRetries, err) {
switch(err.errorCode) {
case ErrorCode.OPERATION_LIMIT_EXCEEDED:
return Config._backoffDelay(numRetries,
req.opt.retry.controlOpBaseDelay);
case ErrorCode.SECURITY_INFO_UNAVAILABLE:
return Config._secInfoNotReadyDelay(numRetries,
req.opt.retry.secInfoNumBackoff,
req.opt.retry.secInfoBaseDelay);
default:
return Config._backoffDelay(numRetries, req.opt.retry.baseDelay);
}
}
//Validate and make uniform interface for retry handler
static _initRetry(cfg) {
if (cfg.retry == null) {
cfg.retry = {};
} else if (typeof cfg.retry !== 'object') {
throw new NoSQLArgumentError('Invalid retry value', cfg);
}
if (cfg.retry.handler == null) {
cfg.retry.handler = {};
} else if (typeof cfg.retry.handler !== 'object') {
throw new NoSQLArgumentError('Invalid retry.handler value', cfg);
}
if (!cfg.retry.handler.doRetry) {
if (cfg.retry.handler.delay != null) {
throw new NoSQLArgumentError(
'Missing retry.handler.doRetry value', cfg);
}
cfg.retry.handler.doRetry = () => false;
return;
}
if (cfg.retry.handler.doRetry === true) {
cfg.retry.handler.doRetry = () => true;
} else if (typeof cfg.retry.handler.doRetry !== 'function') {
throw new NoSQLArgumentError(
'Invalid retry.handler.doRetry value', cfg);
}
//If using default doRetry, maxRetries must be valid
if (cfg.retry.handler.doRetry === this._shouldRetry &&
!isPosInt32(cfg.retry.maxRetries)) {
throw new NoSQLArgumentError(
'Missing or invalid retry.maxRetries value', cfg);
}
if (isPosInt32(cfg.retry.handler.delay)) {
const val = cfg.retry.handler.delay;
cfg.retry.handler.delay = () => val;
return;
}
if (typeof cfg.retry.handler.delay !== 'function') {
throw new NoSQLArgumentError(
'Invalid retry.handler.delay value', cfg);
}
//If using default delay, the following parameters must be valid
if (cfg.retry.handler.delay === this._retryDelay) {
for(let n of ['baseDelay', 'secInfoBaseDelay']) {
if (!isPosInt32(cfg.retry[n])) {
throw new NoSQLArgumentError(
`Invalid retry.${n} value`, cfg);
}
}
if (!isPosInt32OrZero(cfg.retry.secInfoNumBackoff)) {
throw new NoSQLArgumentError(
'Invalid retry.secInfoNumBackoff value', cfg);
}
if (cfg.retry.controlOpBaseDelay != null &&
!isPosInt32(cfg.retry.controlOpBaseDelay)) {
throw new NoSQLArgumentError(
'Invalid retry.controlOpBaseDelay value', cfg);
}
}
}
static _endpoint2url(cfg) {
let endpoint = cfg.endpoint;
if (endpoint instanceof URL) {
endpoint = endpoint.href;
if (endpoint.endsWith('/')) {
endpoint = endpoint.slice(0, -1);
}
}
let host = endpoint;
let proto;
let port;
let i = endpoint.indexOf('://');
if (i !== -1) {
proto = endpoint.substring(0, i).toLowerCase();
if (proto !== 'http' && proto !== 'https') {
throw new NoSQLArgumentError(`Invalid service protocol \
${proto} in endpoint ${endpoint}`, cfg);
}
host = endpoint.substring(i + 3);
}
if (host.includes('/')) {
throw new NoSQLArgumentError(`Invalid endpoint: ${endpoint}, may \
not contain path`, cfg);
}
const parts = host.split(':');
host = parts[0];
if (!parts.length || parts.length > 2) {
throw new NoSQLArgumentError(`Invalid endpoint: ${endpoint}`,
cfg);
}
if (parts.length === 2) {
port = Number(parts[1]);
if (!isPosInt32(port)) {
throw new NoSQLArgumentError(`Invalid port value ${parts[1]} \
for endpoint ${endpoint}`, cfg);
}
}
/*
* If protocol is not specified and the port isn't 443, assume we're
* using http. Cases where we may use port 80, 8080, or a non-standard
* port include internal testing to the proxy or minicloud.
*/
if (proto == null) {
if (port == null) {
port = 443;
}
proto = port === 443 ? 'https' : 'http';
} else if (port == null) {
port = proto === 'https' ? 443 : 8080;
}
try {
return new URL(`${proto}://${host}:${port}`);
} catch(err) {
throw new NoSQLArgumentError(`Invalid endpoint: ${endpoint}, \
failed to construct URL`, cfg);
}
}
static initUrl(cfg, isStrict) {
if (cfg.url != null) {
throw new NoSQLArgumentError('May not specify property "url", \
use "endpoint" or "region" instead');
}
if (cfg.region != null) {
if (cfg.endpoint != null) {
throw new NoSQLArgumentError('Config may not contain both \
service endpoint and region', cfg);
}
if (typeof cfg.region === 'string') {
cfg.region = Region.fromRegionId(cfg.region);
}
if (!(cfg.region instanceof Region)) {
throw new NoSQLArgumentError('Invalid region', cfg);
}
cfg.endpoint = cfg.region.endpoint;
}
if (cfg.endpoint != null) {
if (typeof cfg.endpoint !== 'string' &&
!(cfg.endpoint instanceof URL)) {
throw new NoSQLArgumentError('Invalid service endpoint', cfg);
}
cfg.url = this._endpoint2url(cfg);
} else if (isStrict) {
throw new NoSQLArgumentError('Missing service endpoint or region',
cfg);
}
}
static _init(cfg) {
if (cfg.serviceType != null) {
if (typeof cfg.serviceType === 'string') {
cfg.serviceType = ServiceType[cfg.serviceType.toUpperCase()];
}
if (!(cfg.serviceType instanceof ServiceType)) {
throw new NoSQLArgumentError(
`Invalid service type: ${cfg.serviceType}`, cfg);
}
}
this.initUrl(cfg);
for(let n of ['timeout', 'ddlTimeout', 'securityInfoTimeout',
'tablePollDelay', 'adminPollDelay', 'maxMemoryMB']) {
if (!isPosInt32(cfg[n])) {
throw new NoSQLArgumentError(
`Invalid ${n} value: ${cfg[n]}`, cfg);
}
}
for(let n of ['tablePollTimeout', 'adminPollTimeout']) {
if (cfg[n] !== Infinity && !isPosInt32(cfg[n])) {
throw new NoSQLArgumentError(
`Invalid ${n} value: ${cfg[n]}`, cfg);
}
}
if (cfg.tablePollTimeout < cfg.tablePollDelay) {
throw new NoSQLArgumentError('Table poll timeout cannot be less \
than table poll delay', cfg);
}
if (typeof cfg.consistency === 'string') {
cfg.consistency = Consistency[cfg.consistency.toUpperCase()];
}
if (!(cfg.consistency instanceof Consistency)) {
throw new NoSQLArgumentError(
`Invalid consistency value: ${cfg.consistency}`, cfg);
}
if (cfg.httpOpt != null && typeof cfg.httpOpt !== 'object') {
throw new NoSQLArgumentError(
`Invalid HTTP options: ${cfg.httpOpt}`, cfg);
}
this._initRetry(cfg);
AuthConfig.init(cfg);
//In the special case the value of url may be set by
//IAMAuthorizationProvider by getting region from OCI config file
//In any case, the url must be set at this point.
if (cfg.url == null) {
throw new NoSQLArgumentError('Missing service endpoint or region',
cfg);
}
if (cfg.dbNumber != null) {
cfg._dbNumber = new NumberTypeHandler(cfg);
}
}
static _shouldInheritDefault(key, val) {
//We inherit default properties only for plain Javascript objects,
//not instances of classes. In addition we don't inherit handlers or
//providers. We assume that in these cases the objects fully
//implement their functionality.
if (!isPlainObject(val)) {
return false;
}
const keyLwr = key.toLowerCase();
return !keyLwr.endsWith('handler') && !keyLwr.endsWith('provider');
}
static _inheritOpt(opt, def) {
for(let [key, val] of Object.entries(opt)) {
//Recurse if the property should also be inherited and default
//has matching key
if (this._shouldInheritDefault(key, val)) {
const defVal = def[key];
if (defVal != null) {
this._inheritOpt(val, defVal);
}
}
}
opt.__proto__ = def;
}
static _copyOpt(opt) {
opt = Object.assign({}, opt);
for(let [key, val] of Object.entries(opt)) {
//Recurse if the property should also be inherited and default
//has matching key
if (isPlainObject(val)) {
opt[key] = this._copyOpt(val);
}
}
return opt;
}
//last argument "req" is only for error reporting
static inheritOpt(opt, def, req) {
if (opt == null) {
opt = {};
} else if (typeof opt !== 'object') {
throw new NoSQLArgumentError('Invalid options object',
req ? req : opt);
}
if (opt.__proto__ !== def) {
this._inheritOpt(opt, def);
}
return opt;
}
static create(cfg) {
if (typeof cfg === 'string') {
try {
cfg = requireNoWP(path.resolve(cfg));
} catch(err) {
throw new NoSQLArgumentError('Error loading configuration ' +
`file ${cfg}`, 'NoSQLClient', err);
}
} else {
if (cfg == null) {
//Make an exception for cloud where default oci config file
//may contain all required configuration (region and OCI
//credentials).
cfg = {
serviceType: ServiceType.CLOUD
};
} else if (typeof cfg !== 'object') {
throw new NoSQLArgumentError(
'Missing or invalid configuration',
'NoSQLClient');
}
}
//Copy cfg to prevent further user's changes from having effect. We
//also copy defaults to make sure all changes during _init() are done
//on separate object.
cfg = this.inheritOpt(this._copyOpt(cfg),
this._copyOpt(this.defaults));
this._init(cfg);
return cfg;
}
static destroy(cfg) {
return AuthConfig.close(cfg);
}
}
//Default configuration values
Config.defaults = Object.freeze({
timeout: 5000,
ddlTimeout: 10000,
securityInfoTimeout: 10000,
tablePollTimeout: Infinity,
tablePollDelay: 1000,
adminPollTimeout: Infinity,
adminPollDelay: 1000,
consistency: Consistency.EVENTUAL,
maxMemoryMB: 1024,
retry: Object.freeze({
maxRetries: 10,
baseDelay: 200,
controlOpBaseDelay: 60000,
secInfoBaseDelay: 100,
secInfoNumBackoff: 10,
handler: Object.freeze({
doRetry: Config._shouldRetry,
delay: Config._retryDelay
})
}),
httpOpt: Object.freeze({
keepAlive: true
}),
auth: AuthConfig.defaults
});
module.exports = Config;