@yugabytedb/pg
Version:
Pure JavaScript PostgreSQL client and native libpq bindings with YugabyteDB smart-driver features
1,339 lines (1,224 loc) • 46.4 kB
JavaScript
var EventEmitter = require('events').EventEmitter
var util = require('util')
var utils = require('./utils')
var sasl = require('./sasl')
var pgPass = require('pgpass')
var TypeOverrides = require('./type-overrides')
var ConnectionParameters = require('./connection-parameters')
var Query = require('./query')
var defaults = require('./defaults')
var Connection = require('./connection')
const dns = require('dns')
const { logger } = require('./logger')
const YB_SERVERS_QUERY = 'SELECT * FROM yb_servers()'
const DEFAULT_FAILED_HOST_TTL_SECONDS = 5
class ServerInfo {
constructor(hostName, port, placementInfo, public_ip, node_type) {
this.hostName = hostName
this.port = port
this.placementInfo = placementInfo
this.public_ip = public_ip
this.node_type = node_type
}
}
class Lock {
constructor() {
this._locked = false
this._ee = new EventEmitter().setMaxListeners(0)
}
acquire() {
return new Promise((resolve) => {
if (!this._locked) {
this._locked = true
return resolve()
}
const tryAcquire = () => {
if (!this._locked) {
this._locked = true
this._ee.removeListener('release', tryAcquire)
return resolve()
}
}
this._ee.on('release', tryAcquire)
})
}
release() {
this._locked = false
setImmediate(() => this._ee.emit('release'))
}
}
const lock = new Lock()
class Client extends EventEmitter {
constructor(config) {
logger.silly("Received connection string " + config)
super()
this.connectionParameters = new ConnectionParameters(config)
this.user = this.connectionParameters.user
this.database = this.connectionParameters.database
this.port = this.connectionParameters.port
this.host = this.connectionParameters.host
this.loadBalance = this.connectionParameters.loadBalance
this.topologyKeys = this.connectionParameters.topologyKeys
this.ybServersRefreshInterval = this.connectionParameters.ybServersRefreshInterval
this.fallbackToTopologyKeysOnly = this.connectionParameters.fallbackToTopologyKeysOnly
this.failedHostReconnectDelaySecs = this.connectionParameters.failedHostReconnectDelaySecs
this.connectionString = config
// "hiding" the password so it doesn't show up in stack traces
// or if the client is console.logged
Object.defineProperty(this, 'password', {
configurable: true,
enumerable: false,
writable: true,
value: this.connectionParameters.password,
})
this.replication = this.connectionParameters.replication
var c = config || {}
this._Promise = c.Promise || global.Promise
this._types = new TypeOverrides(c.types)
this._ending = false
this._connecting = false
this._connected = false
this._connectionError = false
this._queryable = true
this.connection =
c.connection ||
new Connection({
stream: c.stream,
ssl: this.connectionParameters.ssl,
keepAlive: c.keepAlive || false,
keepAliveInitialDelayMillis: c.keepAliveInitialDelayMillis || 0,
encoding: this.connectionParameters.client_encoding || 'utf8',
})
this.queryQueue = []
this.binary = c.binary || defaults.binary
this.processID = null
this.secretKey = null
this.ssl = this.connectionParameters.ssl || false
this.config = config
// prevHostIfUsePublic will store the private host name before replacing
// it with public IP for making the connection
this.prevHostIfUsePublic = this.host
this.urlHost = this.host
// As with Password, make SSL->Key (the private key) non-enumerable.
// It won't show up in stack traces
// or if the client is console.logged
if (this.ssl && this.ssl.key) {
Object.defineProperty(this.ssl, 'key', {
enumerable: false,
})
}
this._connectionTimeoutMillis = c.connectionTimeoutMillis || 0
}
// Control Connection
static controlClient = undefined
// Control Connection host
static controlClientHost = ""
static lastTimeMetaDataFetched = new Date().getTime() / 1000
// Map of host -> connectionCount
static connectionMap = new Map()
// Map of failedHost -> ServerInfo of host
static failedHosts = new Map()
// Map of failedHost -> Time at which host was added to failedHosts Map
static failedHostsTime = new Map()
// Map of Primary Host -> ServerInfo
static hostServerInfoPrimary = new Map()
// Map of RR Host -> ServerInfo
static hostServerInfoRR = new Map()
// Boolean to check if public IP needs to be used or not
static usePublic = false
// Map of preference value as key and the list of placements as its value
static topologyKeyMap = new Map()
_errorAllQueries(err) {
const enqueueError = (query) => {
process.nextTick(() => {
query.handleError(err, this.connection)
})
}
if (this.activeQuery) {
enqueueError(this.activeQuery)
this.activeQuery = null
}
this.queryQueue.forEach(enqueueError)
this.queryQueue.length = 0
}
getLeastLoadedServer(hostsList) {
logger.silly("getLeastLoadedServer(): hostsList" + [...hostsList])
if (hostsList.size === 0) {
return this.host
}
let hostServerInfo;
if (this.connectionParameters.loadBalance === 'any' || this.connectionParameters.loadBalance === 'true') {
hostServerInfo = new Map([...Client.hostServerInfoPrimary, ...Client.hostServerInfoRR]);
} else if (this.connectionParameters.loadBalance === 'only-primary' || this.connectionParameters.loadBalance === 'prefer-primary') {
hostServerInfo = new Map(Client.hostServerInfoPrimary);
} else {
hostServerInfo = new Map(Client.hostServerInfoRR);
}
logger.silly("Potential hosts: " + [...hostServerInfo])
let minConnectionCount = Number.MAX_VALUE
let leastLoadedHosts = []
for (var i = 1; i <= Client.topologyKeyMap.size; i++) {
let hosts = hostsList.keys()
for (let host of hosts) {
let placementInfoOfHost
if (hostServerInfo.has(host)) {
placementInfoOfHost = hostServerInfo.get(host).placementInfo
} else {
continue
}
var toCheckStar = placementInfoOfHost.split('.')
var starPlacementInfoOfHost = toCheckStar[0] + "." + toCheckStar[1] + ".*"
if (!Client.topologyKeyMap.get(i).includes(placementInfoOfHost) && !Client.topologyKeyMap.get(i).includes(starPlacementInfoOfHost)) {
continue
}
let hostCount
if (typeof hostsList.get(host) === 'object') {
hostCount = 0
} else {
hostCount = hostsList.get(host)
}
if (minConnectionCount > hostCount) {
leastLoadedHosts = []
minConnectionCount = hostCount
leastLoadedHosts.push(host)
} else if (minConnectionCount === hostCount) {
leastLoadedHosts.push(host)
}
}
if (leastLoadedHosts.length != 0) {
break
}
}
if (leastLoadedHosts.length === 0) {
if (!(this.connectionParameters.loadBalance === 'prefer-primary' || this.connectionParameters.loadBalance === 'prefer-rr')) {
if (Client.topologyKeyMap.size === 0 || !this.connectionParameters.fallbackToTopologyKeysOnly) {
leastLoadedHosts = this.getHosts(hostsList, hostServerInfo)
}
} else {
leastLoadedHosts = this.getHosts(hostsList, hostServerInfo)
if (leastLoadedHosts.length === 0) {
if (this.connectionParameters.loadBalance === 'prefer-primary') {
leastLoadedHosts = this.getHosts(hostsList, new Map(Client.hostServerInfoRR))
} else {
leastLoadedHosts = this.getHosts(hostsList, new Map(Client.hostServerInfoPrimary))
}
}
}
}
if (leastLoadedHosts.length === 0) {
throw new Error('Could not find a least loaded server.')
}
let randomIdx = Math.floor(Math.random() * leastLoadedHosts.length - 1) + 1
let leastLoadedHost = leastLoadedHosts[randomIdx]
logger.silly("Least loaded servers are " + leastLoadedHosts)
logger.debug("Returning " + leastLoadedHost + " as the least loaded host")
return leastLoadedHost
}
getHosts(hostsList, hostServerInfo) {
let minConnectionCount = Number.MAX_VALUE
let leastLoadedHosts = []
let hosts = hostsList.keys()
for (let value of hosts) {
if (!hostServerInfo.has(value)) {
continue
}
let hostCount
if (typeof hostsList.get(value) === 'object') {
hostCount = 0
} else {
hostCount = hostsList.get(value)
}
if (minConnectionCount > hostCount) {
leastLoadedHosts = []
minConnectionCount = hostCount
leastLoadedHosts.push(value)
} else if (minConnectionCount === hostCount) {
leastLoadedHosts.push(value)
}
}
return leastLoadedHosts
}
isValidKey(key) {
var zones = key.split(':')
if (zones.length == 0 || zones.length > 2) {
logger.warn("Given topology-key " + key + " is invalid")
return false
}
var keyParts = zones[0].split('.')
if (keyParts.length !== 3) {
logger.warn("Given topology-key " + key + " is invalid")
return false
}
if (zones[1] == undefined) {
zones[1] = '1'
}
zones[1] = Number(zones[1])
if (zones[1] < 1 || zones[1] > 10 || isNaN(zones[1]) || !Number.isInteger(zones[1])) {
logger.warn("Given topology-key " + key + " is invalid")
return false
}
logger.silly("Given topology-key " + key + " is valid")
return true
}
incrementConnectionCount() {
let prevCount = 0
let host = this.host
if (Client.usePublic) {
host = this.prevHostIfUsePublic
}
if (Client.connectionMap.has(host)) {
prevCount = Client.connectionMap.get(host)
} else if (Client.failedHosts.has(host)) {
logger.debug("Removing " + host + " from failed host list")
let serverInfo = Client.failedHosts.get(host)
if (serverInfo.node_type === 'primary') {
Client.hostServerInfoPrimary.set(host, serverInfo)
} else if (serverInfo.node_type === 'read_replica') {
Client.hostServerInfoRR.set(host, serverInfo)
}
Client.failedHosts.delete(host)
Client.failedHostsTime.delete(host)
}
Client.connectionMap.set(host, prevCount + 1)
logger.debug("Increasing connection count of " + host + " by 1")
}
_connect(callback) {
logger.silly("connect() is called")
var self = this
if (this.connectionParameters.loadBalance !== 'false' && this._connecting) {
this.connection =
this.config.connection ||
new Connection({
stream: this.config.stream,
ssl: this.connectionParameters.ssl,
keepAlive: this.config.keepAlive || false,
keepAliveInitialDelayMillis: this.config.keepAliveInitialDelayMillis || 0,
encoding: this.connectionParameters.client_encoding || 'utf8',
})
this._connecting = false
}
var con = this.connection
this._connectionCallback = callback
if (this._connecting || this._connected) {
const err = new Error('Client has already been connected. You cannot reuse a client.')
process.nextTick(() => {
callback(err)
})
return
}
this._connecting = true
this.connectionTimeoutHandle
if (this._connectionTimeoutMillis > 0) {
this.connectionTimeoutHandle = setTimeout(() => {
con._ending = true
con.stream.destroy(new Error('timeout expired'))
}, this._connectionTimeoutMillis)
}
if (this.connectionParameters.loadBalance !== 'false') {
if (Client.connectionMap.size && (Client.hostServerInfoPrimary.size || Client.hostServerInfoRR.size)) {
this.host = this.getLeastLoadedServer(Client.connectionMap)
if (Client.hostServerInfoPrimary.has(this.host)) {
this.port = Client.hostServerInfoPrimary.get(this.host).port
} else if (Client.hostServerInfoRR.has(this.host)) {
this.port = Client.hostServerInfoRR.get(this.host).port
}
logger.silly("Least loaded host received " + this.host + " port " + this.port)
} else if (Client.failedHosts.size) {
//ToDo: Why call getLeastLoadedServer with the failedHosts Map? Is this still required?
this.host = this.getLeastLoadedServer(Client.failedHosts)
this.port = Client.failedHosts.get(this.host).port
logger.silly("Least loaded host from failed host list received " + this.host + " port " + this.port)
}
}
if (Client.usePublic) {
let currentHost = this.host
let serverInfo
if (Client.hostServerInfoPrimary.has(this.host)) {
serverInfo = Client.hostServerInfoPrimary.get(currentHost)
} else if (Client.hostServerInfoRR.has(this.host)) {
serverInfo = Client.hostServerInfoRR.get(currentHost)
}
this.prevHostIfUsePublic = currentHost
this.host = serverInfo.public_ip
logger.silly("Using public ips, host " + this.host)
}
if (this.host && this.host.indexOf('/') === 0) {
con.connect(this.host + '/.s.PGSQL.' + this.port)
} else {
con.connect(this.port, this.host)
}
// once connection is established send startup message
con.on('connect', function () {
if (self.ssl) {
con.requestSsl()
} else {
con.startup(self.getStartupConf())
}
})
con.on('sslconnect', function () {
con.startup(self.getStartupConf())
})
this._attachListeners(con)
con.once('end', () => {
const error = this._ending ? new Error('Connection terminated') : new Error('Connection terminated unexpectedly')
clearTimeout(this.connectionTimeoutHandle)
this._errorAllQueries(error)
if (!this._ending) {
// if the connection is ended without us calling .end()
// on this client then we have an unexpected disconnection
// treat this as an error unless we've already emitted an error
// during connection.
if (this._connecting && !this._connectionError) {
if (this._connectionCallback) {
this._connectionCallback(error)
} else {
this._handleErrorEvent(error)
}
} else if (!this._connectionError) {
if (Client.controlClientHost === this.host && Client.controlClient != undefined) {
logger.silly("Control Connection host might be down, marking control connection as undefined")
Client.controlClient = undefined
}
this._handleErrorEvent(error)
}
}
process.nextTick(() => {
this.emit('end')
})
})
}
attachErrorListenerOnClientConnection(client) {
client.on('error', () => {
if (Client.hostServerInfoPrimary.has(client.host)) {
logger.debug("Not able to connect to primary host " + client.host + ", adding it to failedHosts")
Client.failedHosts.set(client.host, Client.hostServerInfoPrimary.get(client.host))
let start = new Date().getTime();
Client.failedHostsTime.set(client.host, start)
Client.connectionMap.delete(client.host)
Client.hostServerInfoPrimary.delete(client.host)
} else if (Client.hostServerInfoRR.has(client.host)) {
logger.debug("Not able to connect to read replica host " + client.host + ", adding it to failedHosts")
Client.failedHosts.set(client.host, Client.hostServerInfoRR.get(client.host))
let start = new Date().getTime();
Client.failedHostsTime.set(client.host, start)
Client.connectionMap.delete(client.host)
Client.hostServerInfoRR.delete(client.host)
}
logger.silly("Control Connection host is down, marking control connection as undefined")
Client.controlClient = undefined
})
}
async iterateHostList(client) {
logger.silly("hostServerInfoPrimary: " + [...Client.hostServerInfoPrimary])
logger.silly("hostServerInfoRR: " + [...Client.hostServerInfoRR])
logger.silly("failedHosts: " + [...Client.failedHosts])
let upHostsList = [...Client.hostServerInfoPrimary.keys(), ...Client.hostServerInfoRR.keys()][Symbol.iterator]()
let upHost = upHostsList.next()
let hostIsUp = false
while (upHost.value !== undefined && !hostIsUp) {
if (Client.failedHosts.has(upHost.value)) {
logger.silly(upHost.value + " is present in failed host list, trying next host")
upHost = upHostsList.next()
continue
}
client.host = upHost.value
client.connectionParameters.host = client.host
logger.debug("Trying to create control connection to " + client.host)
await client
.nowConnect()
.then((res) => {
hostIsUp = true
})
.catch((err) => {
client.connection =
client.config.connection ||
new Connection({
stream: client.config.stream,
ssl: client.connectionParameters.ssl,
keepAlive: client.config.keepAlive || false,
keepAliveInitialDelayMillis: client.config.keepAliveInitialDelayMillis || 0,
encoding: client.connectionParameters.client_encoding || 'utf8',
})
logger.debug("Not able to create control connection to host " + client.host + " adding it to failedHosts")
Client.failedHosts.set(client.host, Client.hostServerInfo.get(client.host))
let start = new Date().getTime();
Client.failedHostsTime.set(client.host, start)
Client.connectionMap.delete(client.host)
Client.hostServerInfoPrimary.delete(client.host)
Client.hostServerInfoRR.delete(client.host)
client._connecting = false
upHost = upHostsList.next()
})
}
if (!hostIsUp) {
logger.debug("Not able to create control connection to any host in the cluster")
throw new Error('Not able to create control connection to any host in the cluster')
}
}
async getConnection() {
logger.silly("Creating control connection...")
let currConnectionString = this.connectionString
let client;
if (typeof currConnectionString !== "string") {
currConnectionString.connectionTimeoutMillis = 10000
client = new Client(currConnectionString)
} else {
client = new Client({
connectionString: currConnectionString,
connectionTimeoutMillis: 10000,
});
}
this.attachErrorListenerOnClientConnection(client)
let lookup = util.promisify(dns.lookup)
let addresses = [{ address: client.host }]
await lookup(client.host, { family: 0, all: true }).then((res) => {
addresses = res
})
client.host = addresses[0].address // If both resolved then - IPv6 else IPv4
client.loadBalance = 'false'
client.connectionParameters.loadBalance = 'false'
client.topologyKeys = ''
client.connectionParameters.topologyKeys = ''
if (Client.failedHosts.has(client.host)) {
await this.iterateHostList(client)
} else {
client.connectionParameters.host = client.host
logger.debug("Attempting to create control connection to " + client.host)
await client.nowConnect().catch(async (err) => {
client.connection =
client.config.connection ||
new Connection({
stream: client.config.stream,
ssl: client.connectionParameters.ssl,
keepAlive: client.config.keepAlive || false,
keepAliveInitialDelayMillis: client.config.keepAliveInitialDelayMillis || 0,
encoding: client.connectionParameters.client_encoding || 'utf8',
})
client._connecting = false
logger.silly("Got error: " + err.message + " when attempting to create control connection to " + client.host)
if (addresses.length === 2) {
// If both resolved
client.host = addresses[1].address // IPv4
if (Client.failedHosts.has(client.host)) {
await this.iterateHostList(client)
} else {
client.connectionParameters.host = client.host
logger.silly("Attempting to create control connection to " + client.host)
await client.nowConnect()
}
}
})
}
Client.controlClientHost = client.host
logger.debug("Created control connection to host " + client.host)
return client
}
async getServersInfo() {
logger.silly("Refreshing server info")
var client = Client.controlClient
var result
await client
.query({
text: YB_SERVERS_QUERY,
statement_timeout: 10000, // Timeout after 10 seconds
})
.then((res) => {
result = res
})
.catch((err) => {
this.getConnection()
.then(async (res) => {
Client.controlClient = res
await this.getServersInfo()
})
.catch((err) => {
return this.nowConnect(callback)
})
})
return result
}
createServersList(data) {
logger.silly("Creating servers list")
Client.hostServerInfoPrimary.clear()
Client.hostServerInfoRR.clear()
data.forEach((eachServer) => {
var placementInfo = eachServer.cloud + '.' + eachServer.region + '.' + eachServer.zone
var nodeType = eachServer.node_type
var server = new ServerInfo(eachServer.host, eachServer.port, placementInfo, eachServer.public_ip, eachServer.node_type)
if (nodeType === 'primary') {
Client.hostServerInfoPrimary.set(eachServer.host, server)
if (eachServer.public_ip === this.host) {
Client.usePublic = true
}
} else {
Client.hostServerInfoRR.set(eachServer.host, server)
if (eachServer.public_ip === this.host) {
Client.usePublic = true
}
}
})
logger.debug("Updated hostServerInfoPrimary to " + [...Client.hostServerInfoPrimary] + " and usePublic to " + Client.usePublic)
logger.debug("Updated hostServerInfoRR to " + [...Client.hostServerInfoRR] + " and usePublic to " + Client.usePublic)
}
createConnectionMap(data) {
logger.silly("Creating connection map")
const currConnectionMap = new Map(Client.connectionMap)
Client.connectionMap.clear()
data.forEach((eachServer) => {
if (!Client.failedHosts.has(eachServer.host)) {
if (currConnectionMap.has(eachServer.host)) {
Client.connectionMap.set(eachServer.host, currConnectionMap.get(eachServer.host))
} else {
Client.connectionMap.set(eachServer.host, 0)
}
} else {
let start = new Date().getTime();
if (start - Client.failedHostsTime.get(eachServer.host) > (this.connectionParameters.failedHostReconnectDelaySecs * 1000)) {
logger.debug("Removing " + eachServer.host + " from failed host list")
Client.connectionMap.set(eachServer.host, 0)
Client.failedHosts.delete(eachServer.host)
Client.failedHostsTime.delete(eachServer.host)
}
}
})
logger.debug("Updated connection map " + [...Client.connectionMap])
}
createTopologyKeyMap() {
logger.silly("Creating Topology key map")
var seperatedKeys = this.connectionParameters.topologyKeys.split(',')
for (let idx = 0; idx < seperatedKeys.length; idx++) {
let key = seperatedKeys[idx]
if (this.isValidKey(key)) {
var zones = key.split(':')
if (zones[1] == undefined) {
zones[1] = '1'
}
zones[1] = parseInt(zones[1])
if (Client.topologyKeyMap.has(zones[1])) {
let currentzones = Client.topologyKeyMap.get(zones[1])
currentzones.push(zones[0])
Client.topologyKeyMap.set(zones[1], currentzones)
} else {
Client.topologyKeyMap.set(zones[1], [zones[0]])
}
} else {
throw new Error('Bad Topology Key found - ' + key)
}
}
logger.debug("Updated topologyKey Map " + [...Client.topologyKeyMap])
}
createMetaData(data) {
logger.silly("Creating metadata ...")
this.createServersList(data)
Client.lastTimeMetaDataFetched = new Date().getTime() / 1000
this.createConnectionMap(data)
if (this.connectionParameters.topologyKeys !== '') {
this.createTopologyKeyMap()
}
}
nowConnect(callback) {
logger.silly("nowConnect() is called...")
if (callback) {
logger.silly("callback is not null")
if (this.connectionParameters.loadBalance !== 'false') {
this._connect((error) => {
if (error) {
if (this.connectionParameters.loadBalance !== 'false') {
if (Client.hostServerInfoPrimary.has(this.host)) {
logger.debug("Adding " + this.host + " to failed host list")
Client.failedHosts.set(this.host, Client.hostServerInfoPrimary.get(this.host))
let start = new Date().getTime();
Client.failedHostsTime.set(this.host, start)
Client.connectionMap.delete(this.host)
Client.hostServerInfoPrimary.delete(this.host)
} else if (Client.hostServerInfoRR.has(this.host)) {
logger.debug("Adding " + this.host + " to failed host list")
Client.failedHosts.set(this.host, Client.hostServerInfoRR.get(this.host))
let start = new Date().getTime();
Client.failedHostsTime.set(this.host, start)
Client.connectionMap.delete(this.host)
Client.hostServerInfoRR.delete(this.host)
} else if (Client.failedHosts.has(this.host)) {
logger.silly("Removing host " + this.host + " from failed hosts")
Client.failedHosts.delete(this.host)
Client.failedHostsTime.delete(this.host)
}
lock.release()
this.connect(callback)
} else {
callback(error)
return
}
} else {
if (this.connectionParameters.loadBalance !== 'false') {
lock.release()
this.incrementConnectionCount()
}
callback()
}
})
return
} else {
this._connect(callback)
return
}
}
return new this._Promise((resolve, reject) => {
this._connect((error) => {
if (error) {
logger.silly("Not able to connect to " + this.host + " due to error " + error.message)
if (this.connectionParameters.loadBalance !== 'false' && (Client.hostServerInfoPrimary.size !== 0 || Client.hostServerInfoRR.size !== 0)) {
if (Client.hostServerInfoPrimary.has(this.host)) {
logger.debug("Adding " + this.host + " to failed host list")
Client.failedHosts.set(this.host, Client.hostServerInfoPrimary.get(this.host))
let start = new Date().getTime();
Client.failedHostsTime.set(this.host, start)
Client.connectionMap.delete(this.host)
Client.hostServerInfoPrimary.delete(this.host)
} else if (Client.hostServerInfoRR.has(this.host)) {
logger.debug("Adding " + this.host + " to failed host list")
Client.failedHosts.set(this.host, Client.hostServerInfoRR.get(this.host))
let start = new Date().getTime();
Client.failedHostsTime.set(this.host, start)
Client.connectionMap.delete(this.host)
Client.hostServerInfoRR.delete(this.host)
} else if (Client.failedHosts.has(this.host)) {
//ToDo: Why remove the host from failedHosts? Is this still required?
logger.silly("Removing host " + this.host + " from failed host list")
Client.failedHosts.delete(this.host)
Client.failedHostsTime.delete(this.host)
}
lock.release()
this.connect(callback)
} else {
reject(error)
}
} else {
if (this.connectionParameters.loadBalance !== 'false') {
lock.release()
this.incrementConnectionCount()
}
resolve()
}
})
})
}
updateConnectionMapAfterRefresh() {
logger.silly("Updating connection map after refresh")
let hostsInfoList = [...Client.hostServerInfoPrimary.keys(), ...Client.hostServerInfoRR.keys()]
for (let eachHost of hostsInfoList) {
if (!Client.connectionMap.has(eachHost)) {
if (!Client.failedHosts.has(eachHost)) {
Client.connectionMap.set(eachHost, 0)
} else {
let start = new Date().getTime();
if (start - Client.failedHostsTime.get(eachHost) > (DEFAULT_FAILED_HOST_TTL_SECONDS * 1000)) {
logger.debug("Removing " + eachHost + " from failed host list")
Client.connectionMap.set(eachHost, 0)
Client.failedHosts.delete(eachHost)
Client.failedHostsTime.delete(eachHost)
}
}
}
}
let connectionMapHostList = Client.connectionMap.keys()
for (let eachHost of connectionMapHostList) {
if (!Client.hostServerInfoPrimary.has(eachHost) && !Client.hostServerInfoRR.has(eachHost)) {
Client.connectionMap.delete(eachHost)
}
}
logger.debug("Updated connection Map after refresh " + [...Client.connectionMap]);
logger.debug("Updated failed host list after refresh " + [...Client.failedHosts]);
}
updateMetaData(data) {
logger.silly("Updating MetaData")
this.createServersList(data)
Client.lastTimeMetaDataFetched = new Date().getTime() / 1000
this.updateConnectionMapAfterRefresh()
}
isRefreshRequired() {
let currentTime = new Date().getTime() / 1000
let diff = Math.floor(currentTime - Client.lastTimeMetaDataFetched)
if (diff >= this.connectionParameters.ybServersRefreshInterval) {
logger.silly("Refresh is required")
return true
} else {
logger.silly("Refresh is not required")
return false
}
}
connect(callback) {
if (this.connectionParameters.loadBalance === 'false') {
logger.silly("Loadbalance is false, falling to upstream behaviour")
return this.nowConnect(callback)
}
/*
ToDo: We are holding the lock until the user connection gets created.
This looks like an overkill, why is this required?
*/
lock.acquire().then(() => {
logger.silly("loadBalance: " + this.connectionParameters.loadBalance)
logger.silly("topologyKeys: " + this.connectionParameters.topologyKeys)
logger.silly("ybServersRefreshInterval: " + this.connectionParameters.ybServersRefreshInterval)
logger.silly("fallbackToTopologyKeysOnly: " + this.connectionParameters.fallbackToTopologyKeysOnly)
logger.silly("failedHostReconnectDelaySecs: " + this.connectionParameters.failedHostReconnectDelaySecs)
if (Client.controlClient === undefined) {
this.getConnection()
.then(async (res) => {
Client.controlClient = res
this.getServersInfo()
.catch((err) => {
logger.silly("Not able to get servers info due to error " + err.message)
return this.nowConnect(callback)
})
.then((res) => {
try {
this.createMetaData(res.rows)
return this.nowConnect(callback)
} catch (err) {
if (err.message.includes('Bad Topology Key found')) {
throw err
} else {
//ToDo: Why not throw the error?
logger.debug("Error caught: " + err.message)
}
}
})
})
.catch((err) => {
logger.silly("Not able to create control connection to any node due to error " + err.message)
return this.nowConnect(callback)
})
} else {
if (this.isRefreshRequired()) {
this.getServersInfo()
.then((res) => {
this.updateMetaData(res.rows)
if (this.connectionParameters.topologyKeys !== '') {
this.createTopologyKeyMap()
}
return this.nowConnect(callback)
})
.catch((err) => {
logger.silly("Not able to get servers info, error " + err.message)
return this.nowConnect(callback)
})
} else {
let res = this.nowConnect(callback);
if (res instanceof Promise) {
return res
.then(result => {
return result;
})
.catch(error => {
logger.silly("Releasing lock.")
lock.release();
throw error;
});
} else {
return res;
}
}
}
})
}
_attachListeners(con) {
// password request handling
con.on('authenticationCleartextPassword', this._handleAuthCleartextPassword.bind(this))
// password request handling
con.on('authenticationMD5Password', this._handleAuthMD5Password.bind(this))
// password request handling (SASL)
con.on('authenticationSASL', this._handleAuthSASL.bind(this))
con.on('authenticationSASLContinue', this._handleAuthSASLContinue.bind(this))
con.on('authenticationSASLFinal', this._handleAuthSASLFinal.bind(this))
con.on('backendKeyData', this._handleBackendKeyData.bind(this))
con.on('error', this._handleErrorEvent.bind(this))
con.on('errorMessage', this._handleErrorMessage.bind(this))
con.on('readyForQuery', this._handleReadyForQuery.bind(this))
con.on('notice', this._handleNotice.bind(this))
con.on('rowDescription', this._handleRowDescription.bind(this))
con.on('dataRow', this._handleDataRow.bind(this))
con.on('portalSuspended', this._handlePortalSuspended.bind(this))
con.on('emptyQuery', this._handleEmptyQuery.bind(this))
con.on('commandComplete', this._handleCommandComplete.bind(this))
con.on('parseComplete', this._handleParseComplete.bind(this))
con.on('copyInResponse', this._handleCopyInResponse.bind(this))
con.on('copyData', this._handleCopyData.bind(this))
con.on('notification', this._handleNotification.bind(this))
}
// TODO(bmc): deprecate pgpass "built in" integration since this.password can be a function
// it can be supplied by the user if required - this is a breaking change!
_checkPgPass(cb) {
const con = this.connection
if (typeof this.password === 'function') {
this._Promise
.resolve()
.then(() => this.password())
.then((pass) => {
if (pass !== undefined) {
if (typeof pass !== 'string') {
con.emit('error', new TypeError('Password must be a string'))
return
}
this.connectionParameters.password = this.password = pass
} else {
this.connectionParameters.password = this.password = null
}
cb()
})
.catch((err) => {
con.emit('error', err)
})
} else if (this.password !== null) {
cb()
} else {
pgPass(this.connectionParameters, (pass) => {
if (undefined !== pass) {
this.connectionParameters.password = this.password = pass
}
cb()
})
}
}
_handleAuthCleartextPassword(msg) {
this._checkPgPass(() => {
this.connection.password(this.password)
})
}
_handleAuthMD5Password(msg) {
this._checkPgPass(() => {
const hashedPassword = utils.postgresMd5PasswordHash(this.user, this.password, msg.salt)
this.connection.password(hashedPassword)
})
}
_handleAuthSASL(msg) {
this._checkPgPass(() => {
this.saslSession = sasl.startSession(msg.mechanisms)
this.connection.sendSASLInitialResponseMessage(this.saslSession.mechanism, this.saslSession.response)
})
}
_handleAuthSASLContinue(msg) {
sasl.continueSession(this.saslSession, this.password, msg.data)
this.connection.sendSCRAMClientFinalMessage(this.saslSession.response)
}
_handleAuthSASLFinal(msg) {
sasl.finalizeSession(this.saslSession, msg.data)
this.saslSession = null
}
_handleBackendKeyData(msg) {
this.processID = msg.processID
this.secretKey = msg.secretKey
}
_handleReadyForQuery(msg) {
if (this._connecting) {
this._connecting = false
this._connected = true
clearTimeout(this.connectionTimeoutHandle)
// process possible callback argument to Client#connect
if (this._connectionCallback) {
this._connectionCallback(null, this)
// remove callback for proper error handling
// after the connect event
this._connectionCallback = null
}
this.emit('connect')
}
const { activeQuery } = this
this.activeQuery = null
this.readyForQuery = true
if (activeQuery) {
activeQuery.handleReadyForQuery(this.connection)
}
this._pulseQueryQueue()
}
// if we receieve an error event or error message
// during the connection process we handle it here
_handleErrorWhileConnecting(err) {
if (this._connectionError) {
// TODO(bmc): this is swallowing errors - we shouldn't do this
if (this.connectionParameters.loadBalance !== 'false' || Client.controlClient === undefined) {
if (this._connectionCallback) {
return this._connectionCallback(err)
}
this.emit('error', err)
return
}
return
}
this._connectionError = true
clearTimeout(this.connectionTimeoutHandle)
if (this._connectionCallback) {
return this._connectionCallback(err)
}
this.emit('error', err)
}
// if we're connected and we receive an error event from the connection
// this means the socket is dead - do a hard abort of all queries and emit
// the socket error on the client as well
_handleErrorEvent(err) {
if (this._connecting) {
return this._handleErrorWhileConnecting(err)
}
this._queryable = false
this._errorAllQueries(err)
this.emit('error', err)
}
// handle error messages from the postgres backend
_handleErrorMessage(msg) {
if (this._connecting) {
return this._handleErrorWhileConnecting(msg)
}
const activeQuery = this.activeQuery
if (!activeQuery) {
this._handleErrorEvent(msg)
return
}
this.activeQuery = null
activeQuery.handleError(msg, this.connection)
}
_handleRowDescription(msg) {
// delegate rowDescription to active query
this.activeQuery.handleRowDescription(msg)
}
_handleDataRow(msg) {
// delegate dataRow to active query
this.activeQuery.handleDataRow(msg)
}
_handlePortalSuspended(msg) {
// delegate portalSuspended to active query
this.activeQuery.handlePortalSuspended(this.connection)
}
_handleEmptyQuery(msg) {
// delegate emptyQuery to active query
this.activeQuery.handleEmptyQuery(this.connection)
}
_handleCommandComplete(msg) {
// delegate commandComplete to active query
this.activeQuery.handleCommandComplete(msg, this.connection)
}
_handleParseComplete(msg) {
// if a prepared statement has a name and properly parses
// we track that its already been executed so we don't parse
// it again on the same client
if (this.activeQuery.name) {
this.connection.parsedStatements[this.activeQuery.name] = this.activeQuery.text
}
}
_handleCopyInResponse(msg) {
this.activeQuery.handleCopyInResponse(this.connection)
}
_handleCopyData(msg) {
this.activeQuery.handleCopyData(msg, this.connection)
}
_handleNotification(msg) {
this.emit('notification', msg)
}
_handleNotice(msg) {
this.emit('notice', msg)
}
getStartupConf() {
var params = this.connectionParameters
var data = {
user: params.user,
database: params.database,
}
var appName = params.application_name || params.fallback_application_name
if (appName) {
data.application_name = appName
}
if (params.replication) {
data.replication = '' + params.replication
}
if (params.statement_timeout) {
data.statement_timeout = String(parseInt(params.statement_timeout, 10))
}
if (params.idle_in_transaction_session_timeout) {
data.idle_in_transaction_session_timeout = String(parseInt(params.idle_in_transaction_session_timeout, 10))
}
if (params.options) {
data.options = params.options
}
return data
}
cancel(client, query) {
if (client.activeQuery === query) {
var con = this.connection
if (this.host && this.host.indexOf('/') === 0) {
con.connect(this.host + '/.s.PGSQL.' + this.port)
} else {
con.connect(this.port, this.host)
}
// once connection is established send cancel message
con.on('connect', function () {
con.cancel(client.processID, client.secretKey)
})
} else if (client.queryQueue.indexOf(query) !== -1) {
client.queryQueue.splice(client.queryQueue.indexOf(query), 1)
}
}
setTypeParser(oid, format, parseFn) {
return this._types.setTypeParser(oid, format, parseFn)
}
getTypeParser(oid, format) {
return this._types.getTypeParser(oid, format)
}
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
escapeIdentifier(str) {
return '"' + str.replace(/"/g, '""') + '"'
}
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
escapeLiteral(str) {
var hasBackslash = false
var escaped = "'"
for (var i = 0; i < str.length; i++) {
var c = str[i]
if (c === "'") {
escaped += c + c
} else if (c === '\\') {
escaped += c + c
hasBackslash = true
} else {
escaped += c
}
}
escaped += "'"
if (hasBackslash === true) {
escaped = ' E' + escaped
}
return escaped
}
_pulseQueryQueue() {
if (this.readyForQuery === true) {
this.activeQuery = this.queryQueue.shift()
if (this.activeQuery) {
this.readyForQuery = false
this.hasExecuted = true
const queryError = this.activeQuery.submit(this.connection)
if (queryError) {
process.nextTick(() => {
this.activeQuery.handleError(queryError, this.connection)
this.readyForQuery = true
this._pulseQueryQueue()
})
}
} else if (this.hasExecuted) {
this.activeQuery = null
this.emit('drain')
}
}
}
query(config, values, callback) {
// can take in strings, config object or query object
var query
var result
var readTimeout
var readTimeoutTimer
var queryCallback
if (config === null || config === undefined) {
throw new TypeError('Client was passed a null or undefined query')
} else if (typeof config.submit === 'function') {
readTimeout = config.query_timeout || this.connectionParameters.query_timeout
result = query = config
if (typeof values === 'function') {
query.callback = query.callback || values
}
} else {
readTimeout = this.connectionParameters.query_timeout
query = new Query(config, values, callback)
if (!query.callback) {
result = new this._Promise((resolve, reject) => {
query.callback = (err, res) => (err ? reject(err) : resolve(res))
})
}
}
if (readTimeout) {
queryCallback = query.callback
readTimeoutTimer = setTimeout(() => {
var error = new Error('Query read timeout')
process.nextTick(() => {
query.handleError(error, this.connection)
})
queryCallback(error)
// we already returned an error,
// just do nothing if query completes
query.callback = () => {}
// Remove from queue
var index = this.queryQueue.indexOf(query)
if (index > -1) {
this.queryQueue.splice(index, 1)
}
this._pulseQueryQueue()
}, readTimeout)
query.callback = (err, res) => {
clearTimeout(readTimeoutTimer)
queryCallback(err, res)
}
}
if (this.binary && !query.binary) {
query.binary = true
}
if (query._result && !query._result._types) {
query._result._types = this._types
}
if (!this._queryable) {
process.nextTick(() => {
query.handleError(new Error('Client has encountered a connection error and is not queryable'), this.connection)
})
return result
}
if (this._ending) {
process.nextTick(() => {
query.handleError(new Error('Client was closed and is not queryable'), this.connection)
})
return result
}
this.queryQueue.push(query)
this._pulseQueryQueue()
return result
}
ref() {
this.connection.ref()
}
unref() {
this.connection.unref()
}
end(cb) {
this._ending = true
// if we have never connected, then end is a noop, callback immediately
if (!this.connection._connecting) {
if (cb) {
cb()
} else {
return this._Promise.resolve()
}
}
if (this.activeQuery || !this._queryable) {
// if we have an active query we need to force a disconnect
// on the socket - otherwise a hung query could block end forever
this.connection.stream.destroy()
} else {
this.connection.end()
}
lock.acquire().then(() => {
if (this.connectionParameters.loadBalance !== 'false') {
let prevCount = Client.connectionMap.get(this.host)
if (prevCount > 0) {
logger.debug("Decreasing connection count (" + prevCount + ") of " + this.host + " by 1")
Client.connectionMap.set(this.host, prevCount - 1)
}
}
lock.release()
})
if (cb) {
this.connection.once('end', cb)
} else {
return new this._Promise((resolve) => {
this.connection.once('end', resolve)
})
}
}
}
// expose a Query constructor
Client.Query = Query
module.exports = Client