@ragestudio/scylla-odm 0.22.2 → 0.22.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/batch/index.d.ts +3 -3
- package/batch/index.d.ts.map +1 -1
- package/client.d.ts +6 -5
- package/client.d.ts.map +1 -1
- package/client.js +7 -7
- package/client.js.map +1 -1
- package/cql_gen/create_table.d.ts +1 -1
- package/cql_gen/create_table.d.ts.map +1 -1
- package/document/index.d.ts +3 -3
- package/document/index.d.ts.map +1 -1
- package/driver/LICENSE.txt +177 -0
- package/driver/NOTICE.txt +67 -0
- package/driver/auth/index.d.ts +37 -0
- package/driver/auth/index.js +37 -0
- package/driver/auth/no-auth-provider.js +73 -0
- package/driver/auth/plain-text-auth-provider.js +81 -0
- package/driver/auth/provider.js +77 -0
- package/driver/client-options.js +442 -0
- package/driver/client.js +1267 -0
- package/driver/concurrent/index.d.ts +49 -0
- package/driver/concurrent/index.js +366 -0
- package/driver/connection.js +1034 -0
- package/driver/control-connection.js +1282 -0
- package/driver/encoder.js +2316 -0
- package/driver/errors.js +223 -0
- package/driver/execution-options.js +612 -0
- package/driver/execution-profile.js +274 -0
- package/driver/host-connection-pool.js +587 -0
- package/driver/host.js +699 -0
- package/driver/index.d.ts +387 -0
- package/driver/index.js +81 -0
- package/driver/mapping/cache.js +214 -0
- package/driver/mapping/doc-info-adapter.js +171 -0
- package/driver/mapping/index.d.ts +219 -0
- package/driver/mapping/index.js +57 -0
- package/driver/mapping/mapper.js +225 -0
- package/driver/mapping/mapping-handler.js +641 -0
- package/driver/mapping/model-batch-item.js +215 -0
- package/driver/mapping/model-batch-mapper.js +141 -0
- package/driver/mapping/model-mapper.js +315 -0
- package/driver/mapping/model-mapping-info.js +225 -0
- package/driver/mapping/object-selector.js +417 -0
- package/driver/mapping/q.js +156 -0
- package/driver/mapping/query-generator.js +556 -0
- package/driver/mapping/result-mapper.js +123 -0
- package/driver/mapping/result.js +139 -0
- package/driver/mapping/table-mappings.js +133 -0
- package/driver/mapping/tree.js +160 -0
- package/driver/metadata/aggregate.js +79 -0
- package/driver/metadata/client-state.js +119 -0
- package/driver/metadata/data-collection.js +182 -0
- package/driver/metadata/event-debouncer.js +174 -0
- package/driver/metadata/index.d.ts +276 -0
- package/driver/metadata/index.js +1156 -0
- package/driver/metadata/materialized-view.js +49 -0
- package/driver/metadata/schema-function.js +98 -0
- package/driver/metadata/schema-index.js +166 -0
- package/driver/metadata/schema-parser.js +1399 -0
- package/driver/metadata/table-metadata.js +77 -0
- package/driver/operation-state.js +206 -0
- package/driver/policies/address-resolution.js +145 -0
- package/driver/policies/index.d.ts +241 -0
- package/driver/policies/index.js +110 -0
- package/driver/policies/load-balancing.js +970 -0
- package/driver/policies/reconnection.js +166 -0
- package/driver/policies/retry.js +326 -0
- package/driver/policies/speculative-execution.js +150 -0
- package/driver/policies/timestamp-generation.js +176 -0
- package/driver/prepare-handler.js +347 -0
- package/driver/promise-utils.js +191 -0
- package/driver/readers.js +624 -0
- package/driver/request-execution.js +644 -0
- package/driver/request-handler.js +332 -0
- package/driver/requests.js +618 -0
- package/driver/stream-id-stack.js +209 -0
- package/driver/streams.js +745 -0
- package/driver/token.js +325 -0
- package/driver/tokenizer.js +631 -0
- package/driver/types/big-decimal.js +282 -0
- package/driver/types/duration.js +576 -0
- package/driver/types/index.d.ts +486 -0
- package/driver/types/index.js +733 -0
- package/driver/types/inet-address.js +262 -0
- package/driver/types/integer.js +818 -0
- package/driver/types/local-date.js +280 -0
- package/driver/types/local-time.js +299 -0
- package/driver/types/mutable-long.js +385 -0
- package/driver/types/protocol-version.js +391 -0
- package/driver/types/result-set.js +287 -0
- package/driver/types/result-stream.js +164 -0
- package/driver/types/row.js +85 -0
- package/driver/types/time-uuid.js +414 -0
- package/driver/types/tuple.js +103 -0
- package/driver/types/uuid.js +160 -0
- package/driver/types/vector.js +130 -0
- package/driver/types/version-number.js +153 -0
- package/driver/utils.js +1485 -0
- package/driver/writers.js +350 -0
- package/global.d.ts +1 -1
- package/global.d.ts.map +1 -1
- package/index.d.ts +6 -6
- package/index.d.ts.map +1 -1
- package/index.js +6 -6
- package/index.js.map +1 -1
- package/migrate/index.d.ts +1 -1
- package/migrate/index.d.ts.map +1 -1
- package/migrate/index.js +1 -1
- package/migrate/index.js.map +1 -1
- package/model/index.d.ts +6 -6
- package/model/index.d.ts.map +1 -1
- package/model/index.js +10 -10
- package/model/index.js.map +1 -1
- package/operations/countAll.d.ts +1 -1
- package/operations/countAll.d.ts.map +1 -1
- package/operations/delete.d.ts +3 -4
- package/operations/delete.d.ts.map +1 -1
- package/operations/delete.js +1 -1
- package/operations/delete.js.map +1 -1
- package/operations/find.d.ts +2 -2
- package/operations/find.d.ts.map +1 -1
- package/operations/find.js +1 -1
- package/operations/find.js.map +1 -1
- package/operations/findOne.d.ts +2 -2
- package/operations/findOne.d.ts.map +1 -1
- package/operations/findOne.js +1 -1
- package/operations/findOne.js.map +1 -1
- package/operations/insert.d.ts +3 -3
- package/operations/insert.d.ts.map +1 -1
- package/operations/insert.js +2 -2
- package/operations/insert.js.map +1 -1
- package/operations/sync.d.ts +1 -1
- package/operations/sync.d.ts.map +1 -1
- package/operations/sync.js +1 -1
- package/operations/sync.js.map +1 -1
- package/operations/tableExists.d.ts +1 -1
- package/operations/tableExists.d.ts.map +1 -1
- package/operations/update.d.ts +3 -3
- package/operations/update.d.ts.map +1 -1
- package/operations/update.js +2 -2
- package/operations/update.js.map +1 -1
- package/package.json +4 -12
- package/schema/index.d.ts +1 -1
- package/schema/index.d.ts.map +1 -1
- package/types.d.ts +4 -4
- package/types.d.ts.map +1 -1
- package/utils/queryParser.d.ts +1 -1
- package/utils/queryParser.d.ts.map +1 -1
- package/utils/queryParser.js +1 -1
- package/utils/queryParser.js.map +1 -1
- package/utils/typeChecker.d.ts +1 -1
- package/utils/typeChecker.d.ts.map +1 -1
- package/utils/typeChecker.js +1 -1
- package/utils/typeChecker.js.map +1 -1
|
@@ -0,0 +1,1282 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
* or more contributor license agreements. See the NOTICE file
|
|
4
|
+
* distributed with this work for additional information
|
|
5
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
* to you under the Apache License, Version 2.0 (the
|
|
7
|
+
* "License"); you may not use this file except in compliance
|
|
8
|
+
* with the License. You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/
|
|
18
|
+
import events from "events"
|
|
19
|
+
import util from "util"
|
|
20
|
+
import net from "net"
|
|
21
|
+
import dns from "dns"
|
|
22
|
+
|
|
23
|
+
import errors from "./errors.js"
|
|
24
|
+
import { Host, HostMap } from "./host.js"
|
|
25
|
+
import Metadata from "./metadata/index.js"
|
|
26
|
+
import EventDebouncer from "./metadata/event-debouncer.js"
|
|
27
|
+
import Connection from "./connection.js"
|
|
28
|
+
import requests from "./requests.js"
|
|
29
|
+
import utils from "./utils.js"
|
|
30
|
+
import types from "./types/index.js"
|
|
31
|
+
import promiseUtils from "./promise-utils.js"
|
|
32
|
+
|
|
33
|
+
const f = util.format
|
|
34
|
+
|
|
35
|
+
const selectPeers = "SELECT * FROM system.peers"
|
|
36
|
+
const selectLocal = "SELECT * FROM system.local WHERE key='local'"
|
|
37
|
+
const newNodeDelay = 1000
|
|
38
|
+
const metadataQueryAbortTimeout = 2000
|
|
39
|
+
const schemaChangeTypes = {
|
|
40
|
+
created: "CREATED",
|
|
41
|
+
updated: "UPDATED",
|
|
42
|
+
dropped: "DROPPED",
|
|
43
|
+
}
|
|
44
|
+
const supportedProductTypeKey = "PRODUCT_TYPE"
|
|
45
|
+
const supportedDbaas = "DATASTAX_APOLLO"
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Represents a connection used by the driver to receive events and to check the status of the cluster.
|
|
49
|
+
* <p>It uses an existing connection from the hosts' connection pool to maintain the driver metadata up-to-date.</p>
|
|
50
|
+
*/
|
|
51
|
+
class ControlConnection extends events.EventEmitter {
|
|
52
|
+
/**
|
|
53
|
+
* Creates a new instance of <code>ControlConnection</code>.
|
|
54
|
+
* @param {Object} options
|
|
55
|
+
* @param {ProfileManager} profileManager
|
|
56
|
+
* @param {{borrowHostConnection: function, createConnection: function}} [context] An object containing methods to
|
|
57
|
+
* allow dependency injection.
|
|
58
|
+
*/
|
|
59
|
+
constructor(options, profileManager, context) {
|
|
60
|
+
super()
|
|
61
|
+
|
|
62
|
+
this.protocolVersion = null
|
|
63
|
+
this.hosts = new HostMap()
|
|
64
|
+
this.setMaxListeners(0)
|
|
65
|
+
this.log = utils.log
|
|
66
|
+
Object.defineProperty(this, "options", {
|
|
67
|
+
value: options,
|
|
68
|
+
enumerable: false,
|
|
69
|
+
writable: false,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Cluster metadata that is going to be shared between the Client and ControlConnection
|
|
74
|
+
*/
|
|
75
|
+
this.metadata = new Metadata(this.options, this)
|
|
76
|
+
this.initialized = false
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Host used by the control connection
|
|
80
|
+
* @type {Host|null}
|
|
81
|
+
*/
|
|
82
|
+
this.host = null
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Connection used to retrieve metadata and subscribed to events
|
|
86
|
+
* @type {Connection|null}
|
|
87
|
+
*/
|
|
88
|
+
this.connection = null
|
|
89
|
+
|
|
90
|
+
this._addressTranslator = this.options.policies.addressResolution
|
|
91
|
+
this._reconnectionPolicy = this.options.policies.reconnection
|
|
92
|
+
this._reconnectionSchedule = this._reconnectionPolicy.newSchedule()
|
|
93
|
+
this._isShuttingDown = false
|
|
94
|
+
|
|
95
|
+
// Reference to the encoder of the last valid connection
|
|
96
|
+
this._encoder = null
|
|
97
|
+
this._debouncer = new EventDebouncer(
|
|
98
|
+
options.refreshSchemaDelay,
|
|
99
|
+
this.log.bind(this),
|
|
100
|
+
)
|
|
101
|
+
this._profileManager = profileManager
|
|
102
|
+
this._triedHosts = null
|
|
103
|
+
this._resolvedContactPoints = new Map()
|
|
104
|
+
this._contactPoints = new Set()
|
|
105
|
+
|
|
106
|
+
// Timeout used for delayed handling of topology changes
|
|
107
|
+
this._topologyChangeTimeout = null
|
|
108
|
+
// Timeout used for delayed handling of node status changes
|
|
109
|
+
this._nodeStatusChangeTimeout = null
|
|
110
|
+
|
|
111
|
+
if (context && context.borrowHostConnection) {
|
|
112
|
+
this._borrowHostConnection = context.borrowHostConnection
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (context && context.createConnection) {
|
|
116
|
+
this._createConnection = context.createConnection
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Stores the contact point information and what it resolved to.
|
|
122
|
+
* @param {String|null} address
|
|
123
|
+
* @param {String} port
|
|
124
|
+
* @param {String} name
|
|
125
|
+
* @param {Boolean} isIPv6
|
|
126
|
+
*/
|
|
127
|
+
_addContactPoint(address, port, name, isIPv6) {
|
|
128
|
+
if (address === null) {
|
|
129
|
+
// Contact point could not be resolved, store that the resolution came back empty
|
|
130
|
+
this._resolvedContactPoints.set(name, utils.emptyArray)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const portNumber =
|
|
135
|
+
parseInt(port, 10) || this.options.protocolOptions.port
|
|
136
|
+
const endpoint = `${address}:${portNumber}`
|
|
137
|
+
this._contactPoints.add(endpoint)
|
|
138
|
+
|
|
139
|
+
// Use RFC 3986 for IPv4 and IPv6
|
|
140
|
+
const standardEndpoint = !isIPv6
|
|
141
|
+
? endpoint
|
|
142
|
+
: `[${address}]:${portNumber}`
|
|
143
|
+
|
|
144
|
+
let resolvedAddressedByName = this._resolvedContactPoints.get(name)
|
|
145
|
+
|
|
146
|
+
// NODEJS-646
|
|
147
|
+
//
|
|
148
|
+
// We might have a frozen empty array if DNS resolution wasn't working when this name was
|
|
149
|
+
// initially added, and if that's the case we can't add anything. Detect that case and
|
|
150
|
+
// reset to a mutable array.
|
|
151
|
+
if (
|
|
152
|
+
resolvedAddressedByName === undefined ||
|
|
153
|
+
resolvedAddressedByName === utils.emptyArray
|
|
154
|
+
) {
|
|
155
|
+
resolvedAddressedByName = []
|
|
156
|
+
this._resolvedContactPoints.set(name, resolvedAddressedByName)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
resolvedAddressedByName.push(standardEndpoint)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async _parseContactPoint(name) {
|
|
163
|
+
let addressOrName = name
|
|
164
|
+
let port = null
|
|
165
|
+
|
|
166
|
+
if (name.indexOf("[") === 0 && name.indexOf("]:") > 1) {
|
|
167
|
+
// IPv6 host notation [ip]:port (RFC 3986 section 3.2.2)
|
|
168
|
+
const index = name.lastIndexOf("]:")
|
|
169
|
+
addressOrName = name.substr(1, index - 1)
|
|
170
|
+
port = name.substr(index + 2)
|
|
171
|
+
} else if (name.indexOf(":") > 0) {
|
|
172
|
+
// IPv4 or host name with port notation
|
|
173
|
+
const parts = name.split(":")
|
|
174
|
+
if (parts.length === 2) {
|
|
175
|
+
addressOrName = parts[0]
|
|
176
|
+
port = parts[1]
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (net.isIP(addressOrName)) {
|
|
181
|
+
this._addContactPoint(
|
|
182
|
+
addressOrName,
|
|
183
|
+
port,
|
|
184
|
+
name,
|
|
185
|
+
net.isIPv6(addressOrName),
|
|
186
|
+
)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const addresses = await this._resolveAll(addressOrName)
|
|
191
|
+
if (addresses.length > 0) {
|
|
192
|
+
addresses.forEach((addressInfo) =>
|
|
193
|
+
this._addContactPoint(
|
|
194
|
+
addressInfo.address,
|
|
195
|
+
port,
|
|
196
|
+
name,
|
|
197
|
+
addressInfo.isIPv6,
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
} else {
|
|
201
|
+
// Store that we attempted resolving the name but was not found
|
|
202
|
+
this._addContactPoint(null, null, name, false)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Initializes the control connection by establishing a Connection using a suitable protocol
|
|
208
|
+
* version to be used and retrieving cluster metadata.
|
|
209
|
+
*/
|
|
210
|
+
async init() {
|
|
211
|
+
if (this.initialized) {
|
|
212
|
+
// Prevent multiple serial initializations
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!this.options.sni) {
|
|
217
|
+
// Parse and resolve contact points
|
|
218
|
+
await Promise.all(
|
|
219
|
+
this.options.contactPoints.map((name) =>
|
|
220
|
+
this._parseContactPoint(name),
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
} else {
|
|
224
|
+
this.options.contactPoints.forEach((cp) =>
|
|
225
|
+
this._contactPoints.add(cp),
|
|
226
|
+
)
|
|
227
|
+
const address = this.options.sni.address
|
|
228
|
+
const separatorIndex = address.lastIndexOf(":")
|
|
229
|
+
|
|
230
|
+
if (separatorIndex === -1) {
|
|
231
|
+
throw new new errors.DriverInternalError(
|
|
232
|
+
"The SNI endpoint address should contain ip/name and port",
|
|
233
|
+
)()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const nameOrIp = address.substr(0, separatorIndex)
|
|
237
|
+
this.options.sni.port = address.substr(separatorIndex + 1)
|
|
238
|
+
this.options.sni.addressResolver = new utils.AddressResolver({
|
|
239
|
+
nameOrIp,
|
|
240
|
+
dns,
|
|
241
|
+
})
|
|
242
|
+
await this.options.sni.addressResolver.init()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (this._contactPoints.size === 0) {
|
|
246
|
+
throw new errors.NoHostAvailableError(
|
|
247
|
+
{},
|
|
248
|
+
"No host could be resolved",
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await this._initializeConnection()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_setHealthListeners(host, connection) {
|
|
256
|
+
const self = this
|
|
257
|
+
let wasRefreshCalled = 0
|
|
258
|
+
|
|
259
|
+
function removeListeners() {
|
|
260
|
+
host.removeListener("down", downOrIgnoredHandler)
|
|
261
|
+
host.removeListener("ignore", downOrIgnoredHandler)
|
|
262
|
+
connection.removeListener("socketClose", socketClosedHandler)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function startReconnecting(hostDown) {
|
|
266
|
+
if (wasRefreshCalled++ !== 0) {
|
|
267
|
+
// Prevent multiple calls to reconnect
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
removeListeners()
|
|
272
|
+
|
|
273
|
+
if (self._isShuttingDown) {
|
|
274
|
+
// Don't attempt to reconnect when the ControlConnection is being shutdown
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (hostDown) {
|
|
279
|
+
self.log(
|
|
280
|
+
"warning",
|
|
281
|
+
`Host ${host.address} used by the ControlConnection DOWN, ` +
|
|
282
|
+
`connection to ${connection.endpointFriendlyName} will not longer be used`,
|
|
283
|
+
)
|
|
284
|
+
} else {
|
|
285
|
+
self.log(
|
|
286
|
+
"warning",
|
|
287
|
+
`Connection to ${connection.endpointFriendlyName} used by the ControlConnection was closed`,
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
promiseUtils.toBackground(self._refresh())
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function downOrIgnoredHandler() {
|
|
295
|
+
startReconnecting(true)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function socketClosedHandler() {
|
|
299
|
+
startReconnecting(false)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
host.once("down", downOrIgnoredHandler)
|
|
303
|
+
host.once("ignore", downOrIgnoredHandler)
|
|
304
|
+
connection.once("socketClose", socketClosedHandler)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Iterates through the hostIterator and Gets the following open connection.
|
|
309
|
+
* @param {Iterator<Host>} hostIterator
|
|
310
|
+
* @returns {Connection!}
|
|
311
|
+
*/
|
|
312
|
+
_borrowAConnection(hostIterator) {
|
|
313
|
+
let connection = null
|
|
314
|
+
|
|
315
|
+
while (!connection) {
|
|
316
|
+
const item = hostIterator.next()
|
|
317
|
+
const host = item.value
|
|
318
|
+
|
|
319
|
+
if (item.done) {
|
|
320
|
+
throw new errors.NoHostAvailableError(this._triedHosts)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Only check distance once the load-balancing policies have been initialized
|
|
324
|
+
const distance = this._profileManager.getDistance(host)
|
|
325
|
+
if (!host.isUp() || distance === types.distance.ignored) {
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
connection = this._borrowHostConnection(host)
|
|
331
|
+
} catch (err) {
|
|
332
|
+
this._triedHosts[host.address] = err
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return connection
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Iterates through the contact points and tries to open a connection.
|
|
341
|
+
* @param {Iterator<string>} contactPointsIterator
|
|
342
|
+
* @returns {Promise<void>}
|
|
343
|
+
*/
|
|
344
|
+
async _borrowFirstConnection(contactPointsIterator) {
|
|
345
|
+
let connection = null
|
|
346
|
+
|
|
347
|
+
while (!connection) {
|
|
348
|
+
const item = contactPointsIterator.next()
|
|
349
|
+
const contactPoint = item.value
|
|
350
|
+
|
|
351
|
+
if (item.done) {
|
|
352
|
+
throw new errors.NoHostAvailableError(this._triedHosts)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
connection = await this._createConnection(contactPoint)
|
|
357
|
+
} catch (err) {
|
|
358
|
+
this._triedHosts[contactPoint] = err
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (!connection) {
|
|
363
|
+
const err = new errors.NoHostAvailableError(this._triedHosts)
|
|
364
|
+
this.log(
|
|
365
|
+
"error",
|
|
366
|
+
"ControlConnection failed to acquire a connection",
|
|
367
|
+
)
|
|
368
|
+
throw err
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.protocolVersion = connection.protocolVersion
|
|
372
|
+
this._encoder = connection.encoder
|
|
373
|
+
this.connection = connection
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Default implementation for borrowing connections, that can be injected at constructor level */
|
|
377
|
+
_borrowHostConnection(host) {
|
|
378
|
+
// Borrow any open connection, regardless of the keyspace
|
|
379
|
+
return host.borrowConnection()
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Default implementation for creating initial connections, that can be injected at constructor level
|
|
384
|
+
* @param {String} contactPoint
|
|
385
|
+
*/
|
|
386
|
+
async _createConnection(contactPoint) {
|
|
387
|
+
const c = new Connection(contactPoint, null, this.options)
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
await c.openAsync()
|
|
391
|
+
} catch (err) {
|
|
392
|
+
promiseUtils.toBackground(c.closeAsync())
|
|
393
|
+
throw err
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return c
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Gets the info from local and peer metadata, reloads the keyspaces metadata and rebuilds tokens.
|
|
401
|
+
* <p>It throws an error when there's a failure or when reconnecting and there's no connection.</p>
|
|
402
|
+
* @param {Boolean} initializing Determines whether this function was called in order to initialize the control
|
|
403
|
+
* connection the first time
|
|
404
|
+
* @param {Boolean} isReconnecting Determines whether the refresh is being done because the ControlConnection is
|
|
405
|
+
* switching to use this connection to this host.
|
|
406
|
+
*/
|
|
407
|
+
async _refreshHosts(initializing, isReconnecting) {
|
|
408
|
+
// Get a reference to the current connection as it might change from external events
|
|
409
|
+
const c = this.connection
|
|
410
|
+
|
|
411
|
+
if (!c) {
|
|
412
|
+
if (isReconnecting) {
|
|
413
|
+
throw new errors.DriverInternalError(
|
|
414
|
+
"Connection reference has been lost when reconnecting",
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// it's possible that this was called as a result of a topology change, but the connection was lost
|
|
419
|
+
// between scheduling time and now. This will be called again when there is a new connection.
|
|
420
|
+
return
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
this.log("info", "Refreshing local and peers info")
|
|
424
|
+
|
|
425
|
+
const rsLocal = await c.send(
|
|
426
|
+
new requests.QueryRequest(selectLocal),
|
|
427
|
+
null,
|
|
428
|
+
)
|
|
429
|
+
this._setLocalInfo(initializing, isReconnecting, c, rsLocal)
|
|
430
|
+
|
|
431
|
+
if (!this.host) {
|
|
432
|
+
throw new errors.DriverInternalError(
|
|
433
|
+
"Information from system.local could not be retrieved",
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const rsPeers = await c.send(
|
|
438
|
+
new requests.QueryRequest(selectPeers),
|
|
439
|
+
null,
|
|
440
|
+
)
|
|
441
|
+
await this.setPeersInfo(initializing, rsPeers)
|
|
442
|
+
|
|
443
|
+
if (!this.initialized) {
|
|
444
|
+
// resolve protocol version from highest common version among hosts.
|
|
445
|
+
const highestCommon = types.protocolVersion.getHighestCommon(
|
|
446
|
+
c,
|
|
447
|
+
this.hosts,
|
|
448
|
+
)
|
|
449
|
+
const reconnect = highestCommon !== this.protocolVersion
|
|
450
|
+
|
|
451
|
+
// set protocol version on each host.
|
|
452
|
+
this.protocolVersion = highestCommon
|
|
453
|
+
this.hosts.forEach((h) =>
|
|
454
|
+
h.setProtocolVersion(this.protocolVersion),
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
// if protocol version changed, reconnect the control connection with new version.
|
|
458
|
+
if (reconnect) {
|
|
459
|
+
this.log(
|
|
460
|
+
"info",
|
|
461
|
+
`Reconnecting since the protocol version changed to 0x${highestCommon.toString(16)}`,
|
|
462
|
+
)
|
|
463
|
+
c.decreaseVersion(this.protocolVersion)
|
|
464
|
+
await c.closeAsync()
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
await c.openAsync()
|
|
468
|
+
} catch (err) {
|
|
469
|
+
// Close in the background
|
|
470
|
+
promiseUtils.toBackground(c.closeAsync())
|
|
471
|
+
|
|
472
|
+
throw err
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// To acquire metadata we need to specify the cassandra version
|
|
477
|
+
this.metadata.setCassandraVersion(this.host.getCassandraVersion())
|
|
478
|
+
this.metadata.buildTokens(this.hosts)
|
|
479
|
+
|
|
480
|
+
if (!this.options.isMetadataSyncEnabled) {
|
|
481
|
+
this.metadata.initialized = true
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
await this.metadata.refreshKeyspacesInternal(false)
|
|
486
|
+
this.metadata.initialized = true
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async _refreshControlConnection(hostIterator) {
|
|
491
|
+
if (this.options.sni) {
|
|
492
|
+
this.connection = this._borrowAConnection(hostIterator)
|
|
493
|
+
} else {
|
|
494
|
+
try {
|
|
495
|
+
this.connection = this._borrowAConnection(hostIterator)
|
|
496
|
+
} catch (err) {
|
|
497
|
+
/* NODEJS-632: refresh nodes before getting hosts for reconnect since some hostnames may have
|
|
498
|
+
* shifted during the flight. */
|
|
499
|
+
this.log(
|
|
500
|
+
"info",
|
|
501
|
+
"ControlConnection could not reconnect using existing connections. Refreshing contact points and retrying",
|
|
502
|
+
)
|
|
503
|
+
this._contactPoints.clear()
|
|
504
|
+
this._resolvedContactPoints.clear()
|
|
505
|
+
await Promise.all(
|
|
506
|
+
this.options.contactPoints.map((name) =>
|
|
507
|
+
this._parseContactPoint(name),
|
|
508
|
+
),
|
|
509
|
+
)
|
|
510
|
+
const refreshedContactPoints = Array.from(
|
|
511
|
+
this._contactPoints,
|
|
512
|
+
).join(",")
|
|
513
|
+
this.log(
|
|
514
|
+
"info",
|
|
515
|
+
`Refreshed contact points: ${refreshedContactPoints}`,
|
|
516
|
+
)
|
|
517
|
+
await this._initializeConnection()
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Acquires a new connection and refreshes topology and keyspace metadata.
|
|
524
|
+
* <p>When it fails obtaining a connection and there aren't any more hosts, it schedules reconnection.</p>
|
|
525
|
+
* <p>When it fails obtaining the metadata, it marks connection and/or host unusable and retries using the same
|
|
526
|
+
* iterator from query plan / host list</p>
|
|
527
|
+
* @param {Iterator<Host>} [hostIterator]
|
|
528
|
+
*/
|
|
529
|
+
async _refresh(hostIterator) {
|
|
530
|
+
if (this._isShuttingDown) {
|
|
531
|
+
this.log(
|
|
532
|
+
"info",
|
|
533
|
+
"The ControlConnection will not be refreshed as the Client is being shutdown",
|
|
534
|
+
)
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Reset host and connection
|
|
539
|
+
this.host = null
|
|
540
|
+
this.connection = null
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
if (!hostIterator) {
|
|
544
|
+
this.log("info", "Trying to acquire a connection to a new host")
|
|
545
|
+
this._triedHosts = {}
|
|
546
|
+
hostIterator = await promiseUtils.newQueryPlan(
|
|
547
|
+
this._profileManager.getDefaultLoadBalancing(),
|
|
548
|
+
null,
|
|
549
|
+
null,
|
|
550
|
+
)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
await this._refreshControlConnection(hostIterator)
|
|
554
|
+
} catch (err) {
|
|
555
|
+
// There was a failure obtaining a connection or during metadata retrieval
|
|
556
|
+
this.log(
|
|
557
|
+
"error",
|
|
558
|
+
"ControlConnection failed to acquire a connection",
|
|
559
|
+
err,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
if (!this._isShuttingDown) {
|
|
563
|
+
const delay = this._reconnectionSchedule.next().value
|
|
564
|
+
this.log(
|
|
565
|
+
"warning",
|
|
566
|
+
`ControlConnection could not reconnect, scheduling reconnection in ${delay}ms`,
|
|
567
|
+
)
|
|
568
|
+
setTimeout(() => this._refresh(), delay)
|
|
569
|
+
this.emit("newConnection", err)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
this.log(
|
|
576
|
+
"info",
|
|
577
|
+
`ControlConnection connected to ${this.connection.endpointFriendlyName}`,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
await this._refreshHosts(false, true)
|
|
582
|
+
|
|
583
|
+
await this._registerToConnectionEvents()
|
|
584
|
+
} catch (err) {
|
|
585
|
+
this.log(
|
|
586
|
+
"error",
|
|
587
|
+
"ControlConnection failed to retrieve topology and keyspaces information",
|
|
588
|
+
err,
|
|
589
|
+
)
|
|
590
|
+
this._triedHosts[this.connection.endpoint] = err
|
|
591
|
+
|
|
592
|
+
if (err.isSocketError && this.host) {
|
|
593
|
+
this.host.removeFromPool(this.connection)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Retry the whole thing with the same query plan
|
|
597
|
+
return await this._refresh(hostIterator)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
this._reconnectionSchedule = this._reconnectionPolicy.newSchedule()
|
|
601
|
+
this._setHealthListeners(this.host, this.connection)
|
|
602
|
+
this.emit("newConnection", null, this.connection, this.host)
|
|
603
|
+
|
|
604
|
+
this.log(
|
|
605
|
+
"info",
|
|
606
|
+
`ControlConnection connected to ${this.connection.endpointFriendlyName} and up to date`,
|
|
607
|
+
)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Acquires a connection and refreshes topology and keyspace metadata for the first time.
|
|
612
|
+
* @returns {Promise<void>}
|
|
613
|
+
*/
|
|
614
|
+
async _initializeConnection() {
|
|
615
|
+
this.log("info", "Getting first connection")
|
|
616
|
+
|
|
617
|
+
// Reset host and connection
|
|
618
|
+
this.host = null
|
|
619
|
+
this.connection = null
|
|
620
|
+
this._triedHosts = {}
|
|
621
|
+
|
|
622
|
+
// Randomize order of contact points resolved.
|
|
623
|
+
const contactPointsIterator = utils
|
|
624
|
+
.shuffleArray(Array.from(this._contactPoints))
|
|
625
|
+
[Symbol.iterator]()
|
|
626
|
+
|
|
627
|
+
while (true) {
|
|
628
|
+
await this._borrowFirstConnection(contactPointsIterator)
|
|
629
|
+
|
|
630
|
+
this.log(
|
|
631
|
+
"info",
|
|
632
|
+
`ControlConnection using protocol version 0x${this.protocolVersion.toString(
|
|
633
|
+
16,
|
|
634
|
+
)}, connected to ${this.connection.endpointFriendlyName}`,
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
await this._getSupportedOptions()
|
|
639
|
+
await this._refreshHosts(true, true)
|
|
640
|
+
await this._registerToConnectionEvents()
|
|
641
|
+
|
|
642
|
+
// We have a valid connection, leave the loop
|
|
643
|
+
break
|
|
644
|
+
} catch (err) {
|
|
645
|
+
this.log(
|
|
646
|
+
"error",
|
|
647
|
+
"ControlConnection failed to retrieve topology and keyspaces information",
|
|
648
|
+
err,
|
|
649
|
+
)
|
|
650
|
+
this._triedHosts[this.connection.endpoint] = err
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// The healthy connection used to initialize should be part of the Host pool
|
|
655
|
+
this.host.pool.addExistingConnection(this.connection)
|
|
656
|
+
|
|
657
|
+
this.initialized = true
|
|
658
|
+
this._setHealthListeners(this.host, this.connection)
|
|
659
|
+
this.log(
|
|
660
|
+
"info",
|
|
661
|
+
`ControlConnection connected to ${this.connection.endpointFriendlyName}`,
|
|
662
|
+
)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async _getSupportedOptions() {
|
|
666
|
+
const response = await this.connection.send(requests.options, null)
|
|
667
|
+
|
|
668
|
+
// response.supported is a string multi map, decoded as an Object.
|
|
669
|
+
const productType =
|
|
670
|
+
response.supported && response.supported[supportedProductTypeKey]
|
|
671
|
+
if (Array.isArray(productType) && productType[0] === supportedDbaas) {
|
|
672
|
+
this.metadata.setProductTypeAsDbaas()
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async _registerToConnectionEvents() {
|
|
677
|
+
this.connection.on(
|
|
678
|
+
"nodeTopologyChange",
|
|
679
|
+
this._nodeTopologyChangeHandler.bind(this),
|
|
680
|
+
)
|
|
681
|
+
this.connection.on(
|
|
682
|
+
"nodeStatusChange",
|
|
683
|
+
this._nodeStatusChangeHandler.bind(this),
|
|
684
|
+
)
|
|
685
|
+
this.connection.on(
|
|
686
|
+
"nodeSchemaChange",
|
|
687
|
+
this._nodeSchemaChangeHandler.bind(this),
|
|
688
|
+
)
|
|
689
|
+
const request = new requests.RegisterRequest([
|
|
690
|
+
"TOPOLOGY_CHANGE",
|
|
691
|
+
"STATUS_CHANGE",
|
|
692
|
+
"SCHEMA_CHANGE",
|
|
693
|
+
])
|
|
694
|
+
await this.connection.send(request, null)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Handles a TOPOLOGY_CHANGE event
|
|
699
|
+
*/
|
|
700
|
+
_nodeTopologyChangeHandler(event) {
|
|
701
|
+
this.log("info", "Received topology change", event)
|
|
702
|
+
|
|
703
|
+
// all hosts information needs to be refreshed as tokens might have changed
|
|
704
|
+
clearTimeout(this._topologyChangeTimeout)
|
|
705
|
+
|
|
706
|
+
// Use an additional timer to make sure that the refresh hosts is executed only AFTER the delay
|
|
707
|
+
// In this case, the event debouncer doesn't help because it could not honor the sliding delay (ie: processNow)
|
|
708
|
+
this._topologyChangeTimeout = setTimeout(
|
|
709
|
+
() => promiseUtils.toBackground(this._scheduleRefreshHosts()),
|
|
710
|
+
newNodeDelay,
|
|
711
|
+
)
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Handles a STATUS_CHANGE event
|
|
716
|
+
*/
|
|
717
|
+
_nodeStatusChangeHandler(event) {
|
|
718
|
+
const self = this
|
|
719
|
+
const addressToTranslate = event.inet.address.toString()
|
|
720
|
+
const port = this.options.protocolOptions.port
|
|
721
|
+
this._addressTranslator.translate(
|
|
722
|
+
addressToTranslate,
|
|
723
|
+
port,
|
|
724
|
+
function translateCallback(endPoint) {
|
|
725
|
+
const host = self.hosts.get(endPoint)
|
|
726
|
+
if (!host) {
|
|
727
|
+
self.log(
|
|
728
|
+
"warning",
|
|
729
|
+
"Received status change event but host was not found: " +
|
|
730
|
+
addressToTranslate,
|
|
731
|
+
)
|
|
732
|
+
return
|
|
733
|
+
}
|
|
734
|
+
const distance = self._profileManager.getDistance(host)
|
|
735
|
+
if (event.up) {
|
|
736
|
+
if (distance === types.distance.ignored) {
|
|
737
|
+
return host.setUp(true)
|
|
738
|
+
}
|
|
739
|
+
clearTimeout(self._nodeStatusChangeTimeout)
|
|
740
|
+
// Waits a couple of seconds before marking it as UP
|
|
741
|
+
self._nodeStatusChangeTimeout = setTimeout(
|
|
742
|
+
() => host.checkIsUp(),
|
|
743
|
+
newNodeDelay,
|
|
744
|
+
)
|
|
745
|
+
return
|
|
746
|
+
}
|
|
747
|
+
// marked as down
|
|
748
|
+
if (distance === types.distance.ignored) {
|
|
749
|
+
return host.setDown()
|
|
750
|
+
}
|
|
751
|
+
self.log(
|
|
752
|
+
"warning",
|
|
753
|
+
"Received status change to DOWN for host " + host.address,
|
|
754
|
+
)
|
|
755
|
+
},
|
|
756
|
+
)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Handles a SCHEMA_CHANGE event
|
|
761
|
+
*/
|
|
762
|
+
_nodeSchemaChangeHandler(event) {
|
|
763
|
+
this.log("info", "Schema change", event)
|
|
764
|
+
if (!this.options.isMetadataSyncEnabled) {
|
|
765
|
+
return
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
promiseUtils.toBackground(this.handleSchemaChange(event, false))
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Schedules metadata refresh and callbacks when is refreshed.
|
|
773
|
+
* @param {{keyspace: string, isKeyspace: boolean, schemaChangeType, table, udt, functionName, aggregate}} event
|
|
774
|
+
* @param {Boolean} processNow
|
|
775
|
+
* @returns {Promise<void>}
|
|
776
|
+
*/
|
|
777
|
+
handleSchemaChange(event, processNow) {
|
|
778
|
+
const self = this
|
|
779
|
+
let handler, cqlObject
|
|
780
|
+
|
|
781
|
+
if (event.isKeyspace) {
|
|
782
|
+
if (event.schemaChangeType === schemaChangeTypes.dropped) {
|
|
783
|
+
handler = function removeKeyspace() {
|
|
784
|
+
// if on the same event queue there is a creation, this handler is not going to be executed
|
|
785
|
+
// it is safe to remove the keyspace metadata
|
|
786
|
+
delete self.metadata.keyspaces[event.keyspace]
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return this._scheduleObjectRefresh(
|
|
790
|
+
handler,
|
|
791
|
+
event.keyspace,
|
|
792
|
+
null,
|
|
793
|
+
processNow,
|
|
794
|
+
)
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return this._scheduleKeyspaceRefresh(event.keyspace, processNow)
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const ksInfo = this.metadata.keyspaces[event.keyspace]
|
|
801
|
+
if (!ksInfo) {
|
|
802
|
+
// it hasn't been loaded and it is not part of the metadata, don't mind
|
|
803
|
+
return Promise.resolve()
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (event.table) {
|
|
807
|
+
cqlObject = event.table
|
|
808
|
+
handler = function clearTableState() {
|
|
809
|
+
delete ksInfo.tables[event.table]
|
|
810
|
+
delete ksInfo.views[event.table]
|
|
811
|
+
}
|
|
812
|
+
} else if (event.udt) {
|
|
813
|
+
cqlObject = event.udt
|
|
814
|
+
handler = function clearUdtState() {
|
|
815
|
+
delete ksInfo.udts[event.udt]
|
|
816
|
+
}
|
|
817
|
+
} else if (event.functionName) {
|
|
818
|
+
cqlObject = event.functionName
|
|
819
|
+
handler = function clearFunctionState() {
|
|
820
|
+
delete ksInfo.functions[event.functionName]
|
|
821
|
+
}
|
|
822
|
+
} else if (event.aggregate) {
|
|
823
|
+
cqlObject = event.aggregate
|
|
824
|
+
handler = function clearKeyspaceState() {
|
|
825
|
+
delete ksInfo.aggregates[event.aggregate]
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
if (!handler) {
|
|
830
|
+
// Forward compatibility
|
|
831
|
+
return Promise.resolve()
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// It's a cql object change clean the internal cache
|
|
835
|
+
return this._scheduleObjectRefresh(
|
|
836
|
+
handler,
|
|
837
|
+
event.keyspace,
|
|
838
|
+
cqlObject,
|
|
839
|
+
processNow,
|
|
840
|
+
)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* @param {Function} handler
|
|
845
|
+
* @param {String} keyspace
|
|
846
|
+
* @param {String} cqlObject
|
|
847
|
+
* @param {Boolean} processNow
|
|
848
|
+
* @returns {Promise<void>}
|
|
849
|
+
*/
|
|
850
|
+
_scheduleObjectRefresh(handler, keyspace, cqlObject, processNow) {
|
|
851
|
+
return this._debouncer.eventReceived(
|
|
852
|
+
{ handler, keyspace, cqlObject },
|
|
853
|
+
processNow,
|
|
854
|
+
)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* @param {String} keyspace
|
|
859
|
+
* @param {Boolean} processNow
|
|
860
|
+
* @returns {Promise<void>}
|
|
861
|
+
*/
|
|
862
|
+
_scheduleKeyspaceRefresh(keyspace, processNow) {
|
|
863
|
+
return this._debouncer.eventReceived(
|
|
864
|
+
{
|
|
865
|
+
handler: () => this.metadata.refreshKeyspace(keyspace),
|
|
866
|
+
keyspace,
|
|
867
|
+
},
|
|
868
|
+
processNow,
|
|
869
|
+
)
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/** @returns {Promise<void>} */
|
|
873
|
+
_scheduleRefreshHosts() {
|
|
874
|
+
return this._debouncer.eventReceived(
|
|
875
|
+
{
|
|
876
|
+
handler: () => this._refreshHosts(false, false),
|
|
877
|
+
all: true,
|
|
878
|
+
},
|
|
879
|
+
false,
|
|
880
|
+
)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Sets the information for the host used by the control connection.
|
|
885
|
+
* @param {Boolean} initializing
|
|
886
|
+
* @param {Connection} c
|
|
887
|
+
* @param {Boolean} setCurrentHost Determines if the host retrieved must be set as the current host
|
|
888
|
+
* @param result
|
|
889
|
+
*/
|
|
890
|
+
_setLocalInfo(initializing, setCurrentHost, c, result) {
|
|
891
|
+
if (!result || !result.rows || !result.rows.length) {
|
|
892
|
+
this.log("warning", "No local info could be obtained")
|
|
893
|
+
return
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const row = result.rows[0]
|
|
897
|
+
|
|
898
|
+
let localHost
|
|
899
|
+
|
|
900
|
+
// Note that with SNI enabled, we can trust that rpc_address will contain a valid value.
|
|
901
|
+
const endpoint = !this.options.sni
|
|
902
|
+
? c.endpoint
|
|
903
|
+
: `${row["rpc_address"]}:${this.options.protocolOptions.port}`
|
|
904
|
+
|
|
905
|
+
if (initializing) {
|
|
906
|
+
localHost = new Host(
|
|
907
|
+
endpoint,
|
|
908
|
+
this.protocolVersion,
|
|
909
|
+
this.options,
|
|
910
|
+
this.metadata,
|
|
911
|
+
)
|
|
912
|
+
this.hosts.set(endpoint, localHost)
|
|
913
|
+
this.log("info", `Adding host ${endpoint}`)
|
|
914
|
+
} else {
|
|
915
|
+
localHost = this.hosts.get(endpoint)
|
|
916
|
+
|
|
917
|
+
if (!localHost) {
|
|
918
|
+
this.log("error", "Localhost could not be found")
|
|
919
|
+
return
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
localHost.datacenter = row["data_center"]
|
|
924
|
+
localHost.rack = row["rack"]
|
|
925
|
+
localHost.tokens = row["tokens"]
|
|
926
|
+
localHost.hostId = row["host_id"]
|
|
927
|
+
localHost.cassandraVersion = row["release_version"]
|
|
928
|
+
setDseParameters(localHost, row)
|
|
929
|
+
this.metadata.setPartitioner(row["partitioner"])
|
|
930
|
+
this.log("info", "Local info retrieved")
|
|
931
|
+
|
|
932
|
+
if (setCurrentHost) {
|
|
933
|
+
// Set the host as the one being used by the ControlConnection.
|
|
934
|
+
this.host = localHost
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* @param {Boolean} initializing Determines whether this function was called in order to initialize the control
|
|
940
|
+
* connection the first time.
|
|
941
|
+
* @param {ResultSet} result
|
|
942
|
+
*/
|
|
943
|
+
async setPeersInfo(initializing, result) {
|
|
944
|
+
if (!result || !result.rows) {
|
|
945
|
+
return
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// A map of peers, could useful for in case there are discrepancies
|
|
949
|
+
const peers = {}
|
|
950
|
+
const port = this.options.protocolOptions.port
|
|
951
|
+
const foundDataCenters = new Set()
|
|
952
|
+
|
|
953
|
+
if (this.host && this.host.datacenter) {
|
|
954
|
+
foundDataCenters.add(this.host.datacenter)
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
for (const row of result.rows) {
|
|
958
|
+
const endpoint = await this.getAddressForPeerHost(row, port)
|
|
959
|
+
|
|
960
|
+
if (!endpoint) {
|
|
961
|
+
continue
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
peers[endpoint] = true
|
|
965
|
+
let host = this.hosts.get(endpoint)
|
|
966
|
+
let isNewHost = !host
|
|
967
|
+
|
|
968
|
+
if (isNewHost) {
|
|
969
|
+
host = new Host(
|
|
970
|
+
endpoint,
|
|
971
|
+
this.protocolVersion,
|
|
972
|
+
this.options,
|
|
973
|
+
this.metadata,
|
|
974
|
+
)
|
|
975
|
+
this.log("info", `Adding host ${endpoint}`)
|
|
976
|
+
isNewHost = true
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
host.datacenter = row["data_center"]
|
|
980
|
+
host.rack = row["rack"]
|
|
981
|
+
host.tokens = row["tokens"]
|
|
982
|
+
host.hostId = row["host_id"]
|
|
983
|
+
host.cassandraVersion = row["release_version"]
|
|
984
|
+
setDseParameters(host, row)
|
|
985
|
+
|
|
986
|
+
if (host.datacenter) {
|
|
987
|
+
foundDataCenters.add(host.datacenter)
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (isNewHost) {
|
|
991
|
+
// Add it to the map (and trigger events) after all the properties
|
|
992
|
+
// were set to avoid race conditions
|
|
993
|
+
this.hosts.set(endpoint, host)
|
|
994
|
+
|
|
995
|
+
if (!initializing) {
|
|
996
|
+
// Set the distance at Host level, that way the connection pool is created with the correct settings
|
|
997
|
+
this._profileManager.getDistance(host)
|
|
998
|
+
|
|
999
|
+
// When we are not initializing, we start with the node set as DOWN
|
|
1000
|
+
host.setDown()
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Is there a difference in number between peers + local != hosts
|
|
1006
|
+
if (this.hosts.length > result.rows.length + 1) {
|
|
1007
|
+
// There are hosts in the current state that don't belong (nodes removed or wrong contactPoints)
|
|
1008
|
+
this.log("info", "Removing nodes from the pool")
|
|
1009
|
+
const toRemove = []
|
|
1010
|
+
|
|
1011
|
+
this.hosts.forEach((h) => {
|
|
1012
|
+
//It is not a peer and it is not local host
|
|
1013
|
+
if (!peers[h.address] && h !== this.host) {
|
|
1014
|
+
this.log("info", "Removing host " + h.address)
|
|
1015
|
+
toRemove.push(h.address)
|
|
1016
|
+
h.shutdown(true)
|
|
1017
|
+
}
|
|
1018
|
+
})
|
|
1019
|
+
|
|
1020
|
+
this.hosts.removeMultiple(toRemove)
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (initializing && this.options.localDataCenter) {
|
|
1024
|
+
const localDc = this.options.localDataCenter
|
|
1025
|
+
|
|
1026
|
+
if (!foundDataCenters.has(localDc)) {
|
|
1027
|
+
throw new errors.ArgumentError(
|
|
1028
|
+
`localDataCenter was configured as '${
|
|
1029
|
+
localDc
|
|
1030
|
+
}', but only found hosts in data centers: [${Array.from(foundDataCenters).join(", ")}]`,
|
|
1031
|
+
)
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
this.log("info", "Peers info retrieved")
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Gets the address from a peers row and translates the address.
|
|
1040
|
+
* @param {Object|Row} row
|
|
1041
|
+
* @param {Number} defaultPort
|
|
1042
|
+
* @returns {Promise<string>}
|
|
1043
|
+
*/
|
|
1044
|
+
getAddressForPeerHost(row, defaultPort) {
|
|
1045
|
+
return new Promise((resolve) => {
|
|
1046
|
+
let address = row["rpc_address"]
|
|
1047
|
+
const peer = row["peer"]
|
|
1048
|
+
const bindAllAddress = "0.0.0.0"
|
|
1049
|
+
|
|
1050
|
+
if (!address) {
|
|
1051
|
+
this.log(
|
|
1052
|
+
"error",
|
|
1053
|
+
f(
|
|
1054
|
+
"No rpc_address found for host %s in %s's peers system table. %s will be ignored.",
|
|
1055
|
+
peer,
|
|
1056
|
+
this.host.address,
|
|
1057
|
+
peer,
|
|
1058
|
+
),
|
|
1059
|
+
)
|
|
1060
|
+
return resolve(null)
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (address.toString() === bindAllAddress) {
|
|
1064
|
+
this.log(
|
|
1065
|
+
"warning",
|
|
1066
|
+
f(
|
|
1067
|
+
"Found host with 0.0.0.0 as rpc_address, using listen_address (%s) to contact it instead." +
|
|
1068
|
+
" If this is incorrect you should avoid the use of 0.0.0.0 server side.",
|
|
1069
|
+
peer,
|
|
1070
|
+
),
|
|
1071
|
+
)
|
|
1072
|
+
address = peer
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
this._addressTranslator.translate(
|
|
1076
|
+
address.toString(),
|
|
1077
|
+
defaultPort,
|
|
1078
|
+
resolve,
|
|
1079
|
+
)
|
|
1080
|
+
})
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* Uses the DNS protocol to resolve a IPv4 and IPv6 addresses (A and AAAA records) for the hostname.
|
|
1085
|
+
* It returns an Array of addresses that can be empty and logs the error.
|
|
1086
|
+
* @private
|
|
1087
|
+
* @param name
|
|
1088
|
+
*/
|
|
1089
|
+
async _resolveAll(name) {
|
|
1090
|
+
const addresses = []
|
|
1091
|
+
const resolve4 = util.promisify(dns.resolve4)
|
|
1092
|
+
const resolve6 = util.promisify(dns.resolve6)
|
|
1093
|
+
const lookup = util.promisify(dns.lookup)
|
|
1094
|
+
|
|
1095
|
+
// Ignore errors for resolve calls
|
|
1096
|
+
const ipv4Promise = resolve4(name)
|
|
1097
|
+
.catch(() => {})
|
|
1098
|
+
.then((r) => r || utils.emptyArray)
|
|
1099
|
+
const ipv6Promise = resolve6(name)
|
|
1100
|
+
.catch(() => {})
|
|
1101
|
+
.then((r) => r || utils.emptyArray)
|
|
1102
|
+
|
|
1103
|
+
let arr
|
|
1104
|
+
arr = await ipv4Promise
|
|
1105
|
+
arr.forEach((address) => addresses.push({ address, isIPv6: false }))
|
|
1106
|
+
|
|
1107
|
+
arr = await ipv6Promise
|
|
1108
|
+
arr.forEach((address) => addresses.push({ address, isIPv6: true }))
|
|
1109
|
+
|
|
1110
|
+
if (addresses.length === 0) {
|
|
1111
|
+
// In case dns.resolve*() methods don't yield a valid address for the host name
|
|
1112
|
+
// Use system call getaddrinfo() that might resolve according to host system definitions
|
|
1113
|
+
try {
|
|
1114
|
+
arr = await lookup(name, { all: true })
|
|
1115
|
+
arr.forEach(({ address, family }) =>
|
|
1116
|
+
addresses.push({ address, isIPv6: family === 6 }),
|
|
1117
|
+
)
|
|
1118
|
+
} catch (err) {
|
|
1119
|
+
this.log(
|
|
1120
|
+
"error",
|
|
1121
|
+
`Host with name ${name} could not be resolved`,
|
|
1122
|
+
err,
|
|
1123
|
+
)
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
return addresses
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Waits for a connection to be available. If timeout expires before getting a connection it callbacks in error.
|
|
1132
|
+
* @returns {Promise<void>}
|
|
1133
|
+
*/
|
|
1134
|
+
_waitForReconnection() {
|
|
1135
|
+
return new Promise((resolve, reject) => {
|
|
1136
|
+
const callback = promiseUtils.getCallback(resolve, reject)
|
|
1137
|
+
|
|
1138
|
+
// eslint-disable-next-line prefer-const
|
|
1139
|
+
let timeout
|
|
1140
|
+
|
|
1141
|
+
function newConnectionListener(err) {
|
|
1142
|
+
clearTimeout(timeout)
|
|
1143
|
+
callback(err)
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
this.once("newConnection", newConnectionListener)
|
|
1147
|
+
|
|
1148
|
+
timeout = setTimeout(() => {
|
|
1149
|
+
this.removeListener("newConnection", newConnectionListener)
|
|
1150
|
+
callback(
|
|
1151
|
+
new errors.OperationTimedOutError(
|
|
1152
|
+
"A connection could not be acquired before timeout.",
|
|
1153
|
+
),
|
|
1154
|
+
)
|
|
1155
|
+
}, metadataQueryAbortTimeout)
|
|
1156
|
+
})
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Executes a query using the active connection
|
|
1161
|
+
* @param {String|Request} cqlQuery
|
|
1162
|
+
* @param {Boolean} [waitReconnect] Determines if it should wait for reconnection in case the control connection is not
|
|
1163
|
+
* connected at the moment. Default: true.
|
|
1164
|
+
*/
|
|
1165
|
+
async query(cqlQuery, waitReconnect = true) {
|
|
1166
|
+
const queryOnConnection = async () => {
|
|
1167
|
+
if (!this.connection || this._isShuttingDown) {
|
|
1168
|
+
throw new errors.NoHostAvailableError(
|
|
1169
|
+
{},
|
|
1170
|
+
"ControlConnection is not connected at the time",
|
|
1171
|
+
)
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const request =
|
|
1175
|
+
typeof cqlQuery === "string"
|
|
1176
|
+
? new requests.QueryRequest(cqlQuery, null, null)
|
|
1177
|
+
: cqlQuery
|
|
1178
|
+
return await this.connection.send(request, null)
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
if (!this.connection && waitReconnect) {
|
|
1182
|
+
// Wait until its reconnected (or timer elapses)
|
|
1183
|
+
await this._waitForReconnection()
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
return await queryOnConnection()
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/** @returns {Encoder} The encoder used by the current connection */
|
|
1190
|
+
getEncoder() {
|
|
1191
|
+
if (!this._encoder) {
|
|
1192
|
+
throw new errors.DriverInternalError("Encoder is not defined")
|
|
1193
|
+
}
|
|
1194
|
+
return this._encoder
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Cancels all timers and shuts down synchronously.
|
|
1199
|
+
*/
|
|
1200
|
+
shutdown() {
|
|
1201
|
+
this._isShuttingDown = true
|
|
1202
|
+
this._debouncer.shutdown()
|
|
1203
|
+
// Emit a "newConnection" event with Error, as it may clear timeouts that were waiting new connections
|
|
1204
|
+
this.emit(
|
|
1205
|
+
"newConnection",
|
|
1206
|
+
new errors.DriverError("ControlConnection is being shutdown"),
|
|
1207
|
+
)
|
|
1208
|
+
// Cancel timers
|
|
1209
|
+
clearTimeout(this._topologyChangeTimeout)
|
|
1210
|
+
clearTimeout(this._nodeStatusChangeTimeout)
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Resets the Connection to its initial state.
|
|
1215
|
+
*/
|
|
1216
|
+
async reset() {
|
|
1217
|
+
// Reset the internal state of the ControlConnection for future initialization attempts
|
|
1218
|
+
const currentHosts = this.hosts.clear()
|
|
1219
|
+
|
|
1220
|
+
// Set the shutting down flag temporarily to avoid reconnects.
|
|
1221
|
+
this._isShuttingDown = true
|
|
1222
|
+
|
|
1223
|
+
// Shutdown all individual pools, ignoring any shutdown error
|
|
1224
|
+
await Promise.all(currentHosts.map((h) => h.shutdown()))
|
|
1225
|
+
|
|
1226
|
+
this.initialized = false
|
|
1227
|
+
this._isShuttingDown = false
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Gets a Map containing the original contact points and the addresses that each one resolved to.
|
|
1232
|
+
*/
|
|
1233
|
+
getResolvedContactPoints() {
|
|
1234
|
+
return this._resolvedContactPoints
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Gets the local IP address to which the control connection socket is bound to.
|
|
1239
|
+
* @returns {String|undefined}
|
|
1240
|
+
*/
|
|
1241
|
+
getLocalAddress() {
|
|
1242
|
+
if (!this.connection) {
|
|
1243
|
+
return undefined
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
return this.connection.getLocalAddress()
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Gets the address and port of host the control connection is connected to.
|
|
1251
|
+
* @returns {String|undefined}
|
|
1252
|
+
*/
|
|
1253
|
+
getEndpoint() {
|
|
1254
|
+
if (!this.connection) {
|
|
1255
|
+
return undefined
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
return this.connection.endpoint
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* Parses the DSE workload and assigns it to a host.
|
|
1264
|
+
* @param {Host} host
|
|
1265
|
+
* @param {Row} row
|
|
1266
|
+
* @private
|
|
1267
|
+
*/
|
|
1268
|
+
function setDseParameters(host, row) {
|
|
1269
|
+
if (row["workloads"] !== undefined) {
|
|
1270
|
+
host.workloads = row["workloads"]
|
|
1271
|
+
} else if (row["workload"]) {
|
|
1272
|
+
host.workloads = [row["workload"]]
|
|
1273
|
+
} else {
|
|
1274
|
+
host.workloads = utils.emptyArray
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
if (row["dse_version"] !== undefined) {
|
|
1278
|
+
host.dseVersion = row["dse_version"]
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
export default ControlConnection
|