node-firebird 2.13.0 → 2.14.1
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/README.md +42 -0
- package/lib/callback.d.ts +7 -0
- package/lib/callback.js +16 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +13 -0
- package/lib/pool-cluster.d.ts +89 -0
- package/lib/pool-cluster.js +266 -0
- package/lib/pool.js +2 -10
- package/lib/wire/xsqlvar.js +8 -4
- package/package.json +1 -1
- package/src/callback.ts +15 -0
- package/src/index.ts +15 -0
- package/src/pool-cluster.ts +319 -0
- package/src/pool.ts +3 -10
- package/src/wire/xsqlvar.ts +8 -4
package/README.md
CHANGED
|
@@ -355,6 +355,48 @@ console.log({
|
|
|
355
355
|
is attached, so existing applications keep working unchanged.
|
|
356
356
|
- Metrics are plain getters — reading them has no side effects.
|
|
357
357
|
|
|
358
|
+
#### Multi-host pooling (PoolCluster)
|
|
359
|
+
|
|
360
|
+
For primary/replica topologies (Firebird 4+ logical replication) or plain
|
|
361
|
+
redundancy, `Firebird.poolCluster` manages one pool per named node with
|
|
362
|
+
pattern-based selection and automatic failover — the mysql2 `PoolCluster`
|
|
363
|
+
model:
|
|
364
|
+
|
|
365
|
+
```js
|
|
366
|
+
const cluster = Firebird.poolCluster({
|
|
367
|
+
defaults: { user: 'SYSDBA', password: 'masterkey', database: '/data/app.fdb', connectTimeout: 5000 },
|
|
368
|
+
nodes: {
|
|
369
|
+
primary: { host: 'db-primary' },
|
|
370
|
+
replica1: { host: 'db-replica-1' },
|
|
371
|
+
replica2: { host: 'db-replica-2' },
|
|
372
|
+
},
|
|
373
|
+
max: 4, // per-node pool size
|
|
374
|
+
selector: 'rr', // 'rr' | 'random' | 'order' (first online match)
|
|
375
|
+
removeNodeErrorCount: 5, // offline a node after N consecutive connection failures
|
|
376
|
+
restoreNodeTimeout: 30000, // put it back into rotation after 30s (0 = manual restore())
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// writes go to the primary, reads round-robin across replicas
|
|
380
|
+
await cluster.withConnection('primary', db => db.queryAsync('UPDATE ...'));
|
|
381
|
+
const rows = await cluster.withConnection('replica*', db => db.queryAsync('SELECT ...'));
|
|
382
|
+
|
|
383
|
+
// or bind a pattern once (mysql2's cluster.of)
|
|
384
|
+
const replicas = cluster.of('replica*', 'rr');
|
|
385
|
+
const db = await replicas.getAsync(); // release with db.detach(), as with a plain pool
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
A failed connection attempt marks the node and **fails over** to the next
|
|
389
|
+
matching online node; only when every candidate has failed does the call
|
|
390
|
+
error (set `connectTimeout` in `defaults` so dead-but-routable hosts fail
|
|
391
|
+
fast). Nodes taken offline emit `'offline'`, restorations emit
|
|
392
|
+
`'online'`, and `cluster.status()` returns per-node
|
|
393
|
+
`{ online, errorCount, totalCount, idleCount, activeCount, waitingCount }`
|
|
394
|
+
for monitoring. Each node's pool is a regular
|
|
395
|
+
[connection pool](#pool-events-and-metrics) — health checks, idle
|
|
396
|
+
reaping, `maxUses`/`maxLifetimeMillis` recycling and keepalive all apply
|
|
397
|
+
per node. `add(name, overrides)` / `remove(name)` manage nodes at
|
|
398
|
+
runtime; `destroy()` closes everything.
|
|
399
|
+
|
|
358
400
|
#### Advanced Pooling Features
|
|
359
401
|
|
|
360
402
|
The pool implementation includes several safeguards for reliability:
|
package/lib/callback.d.ts
CHANGED
|
@@ -28,6 +28,13 @@ export declare function toError(err: any): Error;
|
|
|
28
28
|
* Run a callback-style operation and return a Promise for its result.
|
|
29
29
|
* Usage: fromCallback<Database>(cb => attach(options, cb))
|
|
30
30
|
*/
|
|
31
|
+
/**
|
|
32
|
+
* Run `work` with a pooled connection and always return it to its pool
|
|
33
|
+
* (detach) when the promise settles — a detach hiccup never masks the
|
|
34
|
+
* outcome of `work`. Shared by Pool.withConnection and
|
|
35
|
+
* PoolCluster.withConnection so the release semantics cannot drift.
|
|
36
|
+
*/
|
|
37
|
+
export declare function withPooledConnection<T>(getAsync: () => Promise<any>, work: (db: any) => Promise<T> | T): Promise<T>;
|
|
31
38
|
export declare function fromCallback<T = any>(executor: (cb: Callback<T>) => void): Promise<T>;
|
|
32
39
|
export declare function doError(obj: any, callback?: (...args: any[]) => void): void;
|
|
33
40
|
export declare function doCallback<T>(obj: T, callback?: Callback<T>): void;
|
package/lib/callback.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.toError = toError;
|
|
4
|
+
exports.withPooledConnection = withPooledConnection;
|
|
4
5
|
exports.fromCallback = fromCallback;
|
|
5
6
|
exports.doError = doError;
|
|
6
7
|
exports.doCallback = doCallback;
|
|
@@ -23,6 +24,21 @@ function toError(err) {
|
|
|
23
24
|
* Run a callback-style operation and return a Promise for its result.
|
|
24
25
|
* Usage: fromCallback<Database>(cb => attach(options, cb))
|
|
25
26
|
*/
|
|
27
|
+
/**
|
|
28
|
+
* Run `work` with a pooled connection and always return it to its pool
|
|
29
|
+
* (detach) when the promise settles — a detach hiccup never masks the
|
|
30
|
+
* outcome of `work`. Shared by Pool.withConnection and
|
|
31
|
+
* PoolCluster.withConnection so the release semantics cannot drift.
|
|
32
|
+
*/
|
|
33
|
+
async function withPooledConnection(getAsync, work) {
|
|
34
|
+
const db = await getAsync();
|
|
35
|
+
try {
|
|
36
|
+
return await work(db);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await new Promise(function (resolve) { db.detach(function () { resolve(); }); });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
26
42
|
function fromCallback(executor) {
|
|
27
43
|
return new Promise(function (resolve, reject) {
|
|
28
44
|
executor(function (err, result) {
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import Connection from './wire/connection';
|
|
2
|
+
import PoolCluster from './pool-cluster';
|
|
3
|
+
import type { PoolClusterOptions } from './pool-cluster';
|
|
2
4
|
import { escape as escapeValue } from './utils';
|
|
3
5
|
import { parseConnectionUri, parseConnectionString } from './uri';
|
|
4
6
|
import type { Options, SvcMgrOptions, DatabaseCallback, ServiceManagerCallback, SimpleCallback, ConnectionPool, Database, ServiceManager } from './types';
|
|
@@ -41,6 +43,14 @@ export declare function drop(options: Options | string, callback: SimpleCallback
|
|
|
41
43
|
export declare function create(options: Options | string, callback: DatabaseCallback): void;
|
|
42
44
|
export declare function attachOrCreate(options: Options | string, callback: DatabaseCallback): void;
|
|
43
45
|
export declare function pool(max: number, options: Options | string): ConnectionPool;
|
|
46
|
+
/**
|
|
47
|
+
* Multi-host pooling (primaries/replicas, failover): named nodes, each
|
|
48
|
+
* backed by a regular pool, selected by glob pattern + 'rr'/'random'/
|
|
49
|
+
* 'order' selector, with connection-failure failover and error-based
|
|
50
|
+
* node offlining. See README § Multi-host pooling.
|
|
51
|
+
*/
|
|
52
|
+
export declare function poolCluster(options?: PoolClusterOptions): PoolCluster;
|
|
53
|
+
export type { PoolClusterOptions, ClusterSelector } from './pool-cluster';
|
|
44
54
|
export { parseConnectionUri, parseConnectionString };
|
|
45
55
|
export { parseNamedPlaceholders } from './named-params';
|
|
46
56
|
export declare function attachAsync(options: SvcMgrOptions): Promise<ServiceManager>;
|
package/lib/index.js
CHANGED
|
@@ -23,6 +23,7 @@ exports.drop = drop;
|
|
|
23
23
|
exports.create = create;
|
|
24
24
|
exports.attachOrCreate = attachOrCreate;
|
|
25
25
|
exports.pool = pool;
|
|
26
|
+
exports.poolCluster = poolCluster;
|
|
26
27
|
exports.attachAsync = attachAsync;
|
|
27
28
|
exports.createAsync = createAsync;
|
|
28
29
|
exports.attachOrCreateAsync = attachOrCreateAsync;
|
|
@@ -31,6 +32,7 @@ const const_1 = __importDefault(require("./wire/const"));
|
|
|
31
32
|
const callback_1 = require("./callback");
|
|
32
33
|
const connection_1 = __importDefault(require("./wire/connection"));
|
|
33
34
|
const pool_1 = __importDefault(require("./pool"));
|
|
35
|
+
const pool_cluster_1 = __importDefault(require("./pool-cluster"));
|
|
34
36
|
const utils_1 = require("./utils");
|
|
35
37
|
const uri_1 = require("./uri");
|
|
36
38
|
Object.defineProperty(exports, "parseConnectionUri", { enumerable: true, get: function () { return uri_1.parseConnectionUri; } });
|
|
@@ -178,6 +180,17 @@ function attachOrCreate(options, callback) {
|
|
|
178
180
|
function pool(max, options) {
|
|
179
181
|
return new pool_1.default(attach, max, Object.assign({}, (0, uri_1.normalizeOptions)(options), { isPool: true }));
|
|
180
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Multi-host pooling (primaries/replicas, failover): named nodes, each
|
|
185
|
+
* backed by a regular pool, selected by glob pattern + 'rr'/'random'/
|
|
186
|
+
* 'order' selector, with connection-failure failover and error-based
|
|
187
|
+
* node offlining. See README § Multi-host pooling.
|
|
188
|
+
*/
|
|
189
|
+
function poolCluster(options) {
|
|
190
|
+
const normalized = { ...(options || {}) };
|
|
191
|
+
normalized.defaults = (0, uri_1.normalizeOptions)(normalized.defaults || {});
|
|
192
|
+
return new pool_cluster_1.default(attach, normalized);
|
|
193
|
+
}
|
|
181
194
|
var named_params_1 = require("./named-params");
|
|
182
195
|
Object.defineProperty(exports, "parseNamedPlaceholders", { enumerable: true, get: function () { return named_params_1.parseNamedPlaceholders; } });
|
|
183
196
|
function attachAsync(options) {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/***************************************
|
|
2
|
+
*
|
|
3
|
+
* PoolCluster — multi-host pooling (primaries/replicas, failover)
|
|
4
|
+
*
|
|
5
|
+
* The mysql2 PoolCluster model on top of this driver's Pool: named
|
|
6
|
+
* nodes, each backed by a regular connection pool (health checks,
|
|
7
|
+
* recycling and metrics included), selected by glob pattern +
|
|
8
|
+
* selector. Consecutive connection failures take a node offline
|
|
9
|
+
* (with optional timed restoration), and get() fails over to the
|
|
10
|
+
* next matching online node.
|
|
11
|
+
*
|
|
12
|
+
***************************************/
|
|
13
|
+
import Events from 'events';
|
|
14
|
+
import type { Callback } from './callback';
|
|
15
|
+
type AttachFn = (options: any, callback: Callback) => void;
|
|
16
|
+
export type ClusterSelector = 'rr' | 'random' | 'order';
|
|
17
|
+
export interface PoolClusterOptions {
|
|
18
|
+
/** Options shared by every node (user, password, database, …). */
|
|
19
|
+
defaults?: any;
|
|
20
|
+
/** name → per-node option overrides (host, port, …). */
|
|
21
|
+
nodes?: Record<string, any>;
|
|
22
|
+
/** Per-node pool size (default 4). */
|
|
23
|
+
max?: number;
|
|
24
|
+
/** Default selector for get()/of() (default 'rr'). */
|
|
25
|
+
selector?: ClusterSelector;
|
|
26
|
+
/**
|
|
27
|
+
* Consecutive connection failures after which a node goes offline
|
|
28
|
+
* (default 5; 0 disables offlining).
|
|
29
|
+
*/
|
|
30
|
+
removeNodeErrorCount?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Milliseconds after which an offline node is restored and probed
|
|
33
|
+
* again (default 30000; 0 = stay offline until restore()/remove()).
|
|
34
|
+
*/
|
|
35
|
+
restoreNodeTimeout?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Events: 'online' (name) — node restored; 'offline' (name) — node taken
|
|
39
|
+
* out of rotation after too many connection failures; 'remove' (name) —
|
|
40
|
+
* node removed via remove().
|
|
41
|
+
*/
|
|
42
|
+
declare class PoolCluster extends Events.EventEmitter {
|
|
43
|
+
private attach;
|
|
44
|
+
private nodes;
|
|
45
|
+
private rrIndex;
|
|
46
|
+
private max;
|
|
47
|
+
private defaults;
|
|
48
|
+
private selector;
|
|
49
|
+
private removeNodeErrorCount;
|
|
50
|
+
private restoreNodeTimeout;
|
|
51
|
+
private _destroyed;
|
|
52
|
+
constructor(attach: AttachFn, options?: PoolClusterOptions);
|
|
53
|
+
/** Register a node; its pool is created lazily-safe right away. */
|
|
54
|
+
add(name: string, overrides?: any): this;
|
|
55
|
+
/** Remove a node for good, destroying its pool. */
|
|
56
|
+
remove(name: string, callback?: (err?: any) => void): void;
|
|
57
|
+
/** Bring an offline node back into rotation immediately. */
|
|
58
|
+
restore(name: string): void;
|
|
59
|
+
/** name → { online, errorCount, pool metrics } for every node. */
|
|
60
|
+
status(): Record<string, any>;
|
|
61
|
+
private matching;
|
|
62
|
+
private pick;
|
|
63
|
+
private noteFailure;
|
|
64
|
+
/**
|
|
65
|
+
* Acquire a connection from a node matching `pattern` (default '*').
|
|
66
|
+
* Connection failures mark the node and FAIL OVER to the next
|
|
67
|
+
* matching online node; only when every candidate has failed does the
|
|
68
|
+
* callback receive the last error. Release connections with
|
|
69
|
+
* db.detach(), exactly like a plain pool.
|
|
70
|
+
*/
|
|
71
|
+
get(pattern: string | Callback, selector?: ClusterSelector | Callback, callback?: Callback): void;
|
|
72
|
+
getAsync(pattern?: string, selector?: ClusterSelector): Promise<any>;
|
|
73
|
+
/**
|
|
74
|
+
* A pool-like facade bound to a pattern (mysql2's cluster.of):
|
|
75
|
+
* { get, getAsync, withConnection } routed through the cluster's
|
|
76
|
+
* selection and failover.
|
|
77
|
+
*/
|
|
78
|
+
of(pattern: string, selector?: ClusterSelector): {
|
|
79
|
+
get(callback: Callback): void;
|
|
80
|
+
getAsync(): Promise<any>;
|
|
81
|
+
withConnection<T>(work: (db: any) => Promise<T> | T): Promise<T>;
|
|
82
|
+
};
|
|
83
|
+
/** Run `work` with a connection from a matching node, always released. */
|
|
84
|
+
withConnection<T>(pattern: string, work: (db: any) => Promise<T> | T, selector?: ClusterSelector): Promise<T>;
|
|
85
|
+
/** Destroy every node's pool. */
|
|
86
|
+
destroy(callback?: (err?: any) => void): void;
|
|
87
|
+
destroyAsync(): Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
export default PoolCluster;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/***************************************
|
|
3
|
+
*
|
|
4
|
+
* PoolCluster — multi-host pooling (primaries/replicas, failover)
|
|
5
|
+
*
|
|
6
|
+
* The mysql2 PoolCluster model on top of this driver's Pool: named
|
|
7
|
+
* nodes, each backed by a regular connection pool (health checks,
|
|
8
|
+
* recycling and metrics included), selected by glob pattern +
|
|
9
|
+
* selector. Consecutive connection failures take a node offline
|
|
10
|
+
* (with optional timed restoration), and get() fails over to the
|
|
11
|
+
* next matching online node.
|
|
12
|
+
*
|
|
13
|
+
***************************************/
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
const events_1 = __importDefault(require("events"));
|
|
19
|
+
const callback_1 = require("./callback");
|
|
20
|
+
const uri_1 = require("./uri");
|
|
21
|
+
const pool_1 = __importDefault(require("./pool"));
|
|
22
|
+
function patternToRegExp(pattern) {
|
|
23
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
24
|
+
return new RegExp('^' + escaped + '$');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Events: 'online' (name) — node restored; 'offline' (name) — node taken
|
|
28
|
+
* out of rotation after too many connection failures; 'remove' (name) —
|
|
29
|
+
* node removed via remove().
|
|
30
|
+
*/
|
|
31
|
+
class PoolCluster extends events_1.default.EventEmitter {
|
|
32
|
+
constructor(attach, options) {
|
|
33
|
+
super();
|
|
34
|
+
this.nodes = new Map();
|
|
35
|
+
this.rrIndex = new Map();
|
|
36
|
+
this._destroyed = false;
|
|
37
|
+
options = options || {};
|
|
38
|
+
this.attach = attach;
|
|
39
|
+
this.defaults = options.defaults || {};
|
|
40
|
+
this.max = options.max && options.max > 0 ? options.max : 4;
|
|
41
|
+
this.selector = options.selector || 'rr';
|
|
42
|
+
this.removeNodeErrorCount = options.removeNodeErrorCount !== undefined ? options.removeNodeErrorCount : 5;
|
|
43
|
+
this.restoreNodeTimeout = options.restoreNodeTimeout !== undefined ? options.restoreNodeTimeout : 30000;
|
|
44
|
+
for (const [name, overrides] of Object.entries(options.nodes || {})) {
|
|
45
|
+
this.add(name, overrides);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Register a node; its pool is created lazily-safe right away. */
|
|
49
|
+
add(name, overrides) {
|
|
50
|
+
if (this._destroyed) {
|
|
51
|
+
throw new Error('PoolCluster has been destroyed');
|
|
52
|
+
}
|
|
53
|
+
if (this.nodes.has(name)) {
|
|
54
|
+
throw new Error('PoolCluster node already exists: ' + name);
|
|
55
|
+
}
|
|
56
|
+
// a connection-string override must be parsed, not object-spread
|
|
57
|
+
// into character-indexed garbage
|
|
58
|
+
if (typeof overrides === 'string') {
|
|
59
|
+
overrides = (0, uri_1.parseConnectionString)(overrides);
|
|
60
|
+
}
|
|
61
|
+
const nodeOptions = { ...this.defaults, ...(overrides || {}) };
|
|
62
|
+
this.nodes.set(name, {
|
|
63
|
+
name,
|
|
64
|
+
options: nodeOptions,
|
|
65
|
+
pool: new pool_1.default(this.attach, nodeOptions.max || this.max, { ...nodeOptions, isPool: true }),
|
|
66
|
+
online: true,
|
|
67
|
+
errorCount: 0,
|
|
68
|
+
restoreTimer: null,
|
|
69
|
+
});
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
/** Remove a node for good, destroying its pool. */
|
|
73
|
+
remove(name, callback) {
|
|
74
|
+
const node = this.nodes.get(name);
|
|
75
|
+
if (!node) {
|
|
76
|
+
if (callback)
|
|
77
|
+
callback();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.nodes.delete(name);
|
|
81
|
+
if (node.restoreTimer) {
|
|
82
|
+
clearTimeout(node.restoreTimer);
|
|
83
|
+
}
|
|
84
|
+
this.emit('remove', name);
|
|
85
|
+
node.pool.destroy(callback);
|
|
86
|
+
}
|
|
87
|
+
/** Bring an offline node back into rotation immediately. */
|
|
88
|
+
restore(name) {
|
|
89
|
+
const node = this.nodes.get(name);
|
|
90
|
+
if (!node || node.online) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (node.restoreTimer) {
|
|
94
|
+
clearTimeout(node.restoreTimer);
|
|
95
|
+
node.restoreTimer = null;
|
|
96
|
+
}
|
|
97
|
+
node.online = true;
|
|
98
|
+
node.errorCount = 0;
|
|
99
|
+
this.emit('online', name);
|
|
100
|
+
}
|
|
101
|
+
/** name → { online, errorCount, pool metrics } for every node. */
|
|
102
|
+
status() {
|
|
103
|
+
const out = {};
|
|
104
|
+
for (const node of this.nodes.values()) {
|
|
105
|
+
out[node.name] = {
|
|
106
|
+
online: node.online,
|
|
107
|
+
errorCount: node.errorCount,
|
|
108
|
+
totalCount: node.pool.totalCount,
|
|
109
|
+
idleCount: node.pool.idleCount,
|
|
110
|
+
activeCount: node.pool.activeCount,
|
|
111
|
+
waitingCount: node.pool.waitingCount,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
matching(pattern) {
|
|
117
|
+
const re = patternToRegExp(pattern);
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const node of this.nodes.values()) {
|
|
120
|
+
if (re.test(node.name)) {
|
|
121
|
+
out.push(node);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
pick(pattern, selector, exclude) {
|
|
127
|
+
const candidates = this.matching(pattern).filter((n) => n.online && !exclude.has(n.name));
|
|
128
|
+
if (!candidates.length) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
if (selector === 'random') {
|
|
132
|
+
return candidates[Math.floor(Math.random() * candidates.length)];
|
|
133
|
+
}
|
|
134
|
+
if (selector === 'order') {
|
|
135
|
+
return candidates[0];
|
|
136
|
+
}
|
|
137
|
+
// round-robin per pattern; only the FIRST pick of a get() advances
|
|
138
|
+
// the counter — failover re-picks reuse it, or a run of failovers
|
|
139
|
+
// would skew the distribution toward nodes after the failing ones
|
|
140
|
+
const index = this.rrIndex.get(pattern) || 0;
|
|
141
|
+
if (exclude.size === 0) {
|
|
142
|
+
this.rrIndex.set(pattern, index + 1);
|
|
143
|
+
}
|
|
144
|
+
return candidates[index % candidates.length];
|
|
145
|
+
}
|
|
146
|
+
noteFailure(node) {
|
|
147
|
+
// a node removed while a get was in flight must not accumulate
|
|
148
|
+
// counters, emit 'offline', or arm a restore timer nobody clears
|
|
149
|
+
if (!this.nodes.has(node.name)) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
node.errorCount++;
|
|
153
|
+
if (!this.removeNodeErrorCount || node.errorCount < this.removeNodeErrorCount || !node.online) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
node.online = false;
|
|
157
|
+
this.emit('offline', node.name);
|
|
158
|
+
if (this.restoreNodeTimeout > 0) {
|
|
159
|
+
node.restoreTimer = setTimeout(() => {
|
|
160
|
+
node.restoreTimer = null;
|
|
161
|
+
this.restore(node.name);
|
|
162
|
+
}, this.restoreNodeTimeout);
|
|
163
|
+
if (node.restoreTimer.unref) {
|
|
164
|
+
node.restoreTimer.unref();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Acquire a connection from a node matching `pattern` (default '*').
|
|
170
|
+
* Connection failures mark the node and FAIL OVER to the next
|
|
171
|
+
* matching online node; only when every candidate has failed does the
|
|
172
|
+
* callback receive the last error. Release connections with
|
|
173
|
+
* db.detach(), exactly like a plain pool.
|
|
174
|
+
*/
|
|
175
|
+
get(pattern, selector, callback) {
|
|
176
|
+
if (typeof pattern === 'function') {
|
|
177
|
+
callback = pattern;
|
|
178
|
+
pattern = '*';
|
|
179
|
+
}
|
|
180
|
+
if (typeof selector === 'function') {
|
|
181
|
+
callback = selector;
|
|
182
|
+
selector = undefined;
|
|
183
|
+
}
|
|
184
|
+
if (this._destroyed) {
|
|
185
|
+
callback(new Error('PoolCluster has been destroyed'), null);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const sel = selector || this.selector;
|
|
189
|
+
const tried = new Set();
|
|
190
|
+
const self = this;
|
|
191
|
+
const attempt = (lastError) => {
|
|
192
|
+
const node = self.pick(pattern, sel, tried);
|
|
193
|
+
if (!node) {
|
|
194
|
+
callback(lastError || new Error('PoolCluster: no online node matches pattern "' + pattern + '"'), null);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
tried.add(node.name);
|
|
198
|
+
node.pool.get((err, db) => {
|
|
199
|
+
if (err) {
|
|
200
|
+
self.noteFailure(node);
|
|
201
|
+
attempt(err);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
node.errorCount = 0;
|
|
205
|
+
callback(null, db);
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
attempt();
|
|
209
|
+
}
|
|
210
|
+
getAsync(pattern, selector) {
|
|
211
|
+
const self = this;
|
|
212
|
+
return (0, callback_1.fromCallback)((cb) => self.get(pattern || '*', selector, cb));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* A pool-like facade bound to a pattern (mysql2's cluster.of):
|
|
216
|
+
* { get, getAsync, withConnection } routed through the cluster's
|
|
217
|
+
* selection and failover.
|
|
218
|
+
*/
|
|
219
|
+
of(pattern, selector) {
|
|
220
|
+
const self = this;
|
|
221
|
+
return {
|
|
222
|
+
get(callback) {
|
|
223
|
+
self.get(pattern, selector, callback);
|
|
224
|
+
},
|
|
225
|
+
getAsync() {
|
|
226
|
+
return self.getAsync(pattern, selector);
|
|
227
|
+
},
|
|
228
|
+
withConnection(work) {
|
|
229
|
+
return self.withConnection(pattern, work, selector);
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/** Run `work` with a connection from a matching node, always released. */
|
|
234
|
+
withConnection(pattern, work, selector) {
|
|
235
|
+
return (0, callback_1.withPooledConnection)(() => this.getAsync(pattern, selector), work);
|
|
236
|
+
}
|
|
237
|
+
/** Destroy every node's pool. */
|
|
238
|
+
destroy(callback) {
|
|
239
|
+
this._destroyed = true;
|
|
240
|
+
const nodes = [...this.nodes.values()];
|
|
241
|
+
this.nodes.clear();
|
|
242
|
+
let remaining = nodes.length;
|
|
243
|
+
if (!remaining) {
|
|
244
|
+
if (callback)
|
|
245
|
+
callback();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
let firstError = null;
|
|
249
|
+
for (const node of nodes) {
|
|
250
|
+
if (node.restoreTimer) {
|
|
251
|
+
clearTimeout(node.restoreTimer);
|
|
252
|
+
}
|
|
253
|
+
node.pool.destroy((err) => {
|
|
254
|
+
if (err && !firstError)
|
|
255
|
+
firstError = err;
|
|
256
|
+
if (--remaining === 0 && callback)
|
|
257
|
+
callback(firstError);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
destroyAsync() {
|
|
262
|
+
const self = this;
|
|
263
|
+
return (0, callback_1.fromCallback)((cb) => self.destroy(cb));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
exports.default = PoolCluster;
|
package/lib/pool.js
CHANGED
|
@@ -352,16 +352,8 @@ class Pool extends events_1.default.EventEmitter {
|
|
|
352
352
|
* Run `work` with a connection from the pool, returning it to the pool
|
|
353
353
|
* (detach) when the returned promise settles — success or failure.
|
|
354
354
|
*/
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
try {
|
|
358
|
-
return await work(db);
|
|
359
|
-
}
|
|
360
|
-
finally {
|
|
361
|
-
// A pooled detach only returns the connection to the pool; do not
|
|
362
|
-
// let a detach hiccup mask the outcome of `work`.
|
|
363
|
-
await new Promise(function (resolve) { db.detach(function () { resolve(); }); });
|
|
364
|
-
}
|
|
355
|
+
withConnection(work) {
|
|
356
|
+
return (0, callback_1.withPooledConnection)(() => this.getAsync(), work);
|
|
365
357
|
}
|
|
366
358
|
}
|
|
367
359
|
module.exports = Pool;
|
package/lib/wire/xsqlvar.js
CHANGED
|
@@ -637,8 +637,9 @@ class SQLVarTimeTz extends SQLVarBase {
|
|
|
637
637
|
var time = data.readUInt();
|
|
638
638
|
data.readInt(); // skip timezone info
|
|
639
639
|
if (!lowerV13 || !data.readInt()) {
|
|
640
|
+
// the wire value is already UTC; no local-offset adjustment
|
|
640
641
|
var d = new Date(0);
|
|
641
|
-
d.setMilliseconds(Math.floor(time / 10)
|
|
642
|
+
d.setMilliseconds(Math.floor(time / 10));
|
|
642
643
|
return d;
|
|
643
644
|
}
|
|
644
645
|
return null;
|
|
@@ -655,8 +656,9 @@ class SQLVarTimeTzEx extends SQLVarTimeTz {
|
|
|
655
656
|
data.readInt(); // skip timezone info
|
|
656
657
|
data.readInt(); // skip ext_offset
|
|
657
658
|
if (!lowerV13 || !data.readInt()) {
|
|
659
|
+
// the wire value is already UTC; no local-offset adjustment
|
|
658
660
|
var d = new Date(0);
|
|
659
|
-
d.setMilliseconds(Math.floor(time / 10)
|
|
661
|
+
d.setMilliseconds(Math.floor(time / 10));
|
|
660
662
|
return d;
|
|
661
663
|
}
|
|
662
664
|
return null;
|
|
@@ -673,8 +675,9 @@ class SQLVarTimeStampTz extends SQLVarBase {
|
|
|
673
675
|
var time = data.readUInt();
|
|
674
676
|
data.readInt(); // skip timezone info
|
|
675
677
|
if (!lowerV13 || !data.readInt()) {
|
|
678
|
+
// the wire value is already UTC; no local-offset adjustment
|
|
676
679
|
var d = new Date(0);
|
|
677
|
-
d.setMilliseconds((date - DateOffset) * TimeCoeff + Math.floor(time / 10)
|
|
680
|
+
d.setMilliseconds((date - DateOffset) * TimeCoeff + Math.floor(time / 10));
|
|
678
681
|
return d;
|
|
679
682
|
}
|
|
680
683
|
return null;
|
|
@@ -692,8 +695,9 @@ class SQLVarTimeStampTzEx extends SQLVarTimeStampTz {
|
|
|
692
695
|
data.readInt(); // skip timezone info
|
|
693
696
|
data.readInt(); // skip ext_offset
|
|
694
697
|
if (!lowerV13 || !data.readInt()) {
|
|
698
|
+
// the wire value is already UTC; no local-offset adjustment
|
|
695
699
|
var d = new Date(0);
|
|
696
|
-
d.setMilliseconds((date - DateOffset) * TimeCoeff + Math.floor(time / 10)
|
|
700
|
+
d.setMilliseconds((date - DateOffset) * TimeCoeff + Math.floor(time / 10));
|
|
697
701
|
return d;
|
|
698
702
|
}
|
|
699
703
|
return null;
|
package/package.json
CHANGED
package/src/callback.ts
CHANGED
|
@@ -45,6 +45,21 @@ export function toError(err: any): Error {
|
|
|
45
45
|
* Run a callback-style operation and return a Promise for its result.
|
|
46
46
|
* Usage: fromCallback<Database>(cb => attach(options, cb))
|
|
47
47
|
*/
|
|
48
|
+
/**
|
|
49
|
+
* Run `work` with a pooled connection and always return it to its pool
|
|
50
|
+
* (detach) when the promise settles — a detach hiccup never masks the
|
|
51
|
+
* outcome of `work`. Shared by Pool.withConnection and
|
|
52
|
+
* PoolCluster.withConnection so the release semantics cannot drift.
|
|
53
|
+
*/
|
|
54
|
+
export async function withPooledConnection<T>(getAsync: () => Promise<any>, work: (db: any) => Promise<T> | T): Promise<T> {
|
|
55
|
+
const db = await getAsync();
|
|
56
|
+
try {
|
|
57
|
+
return await work(db);
|
|
58
|
+
} finally {
|
|
59
|
+
await new Promise<void>(function(resolve) { db.detach(function() { resolve(); }); });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
export function fromCallback<T = any>(executor: (cb: Callback<T>) => void): Promise<T> {
|
|
49
64
|
return new Promise<T>(function(resolve, reject) {
|
|
50
65
|
executor(function(err?: any, result?: T) {
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ import Const from './wire/const';
|
|
|
2
2
|
import { doError, doCallback, fromCallback, type Callback } from './callback';
|
|
3
3
|
import Connection from './wire/connection';
|
|
4
4
|
import Pool from './pool';
|
|
5
|
+
import PoolCluster from './pool-cluster';
|
|
6
|
+
import type { PoolClusterOptions } from './pool-cluster';
|
|
5
7
|
import { escape as escapeValue } from './utils';
|
|
6
8
|
import { parseConnectionUri, parseConnectionString, normalizeOptions } from './uri';
|
|
7
9
|
import type {
|
|
@@ -194,6 +196,19 @@ export function pool(max: number, options: Options | string): ConnectionPool {
|
|
|
194
196
|
return new Pool(attach, max, Object.assign({}, normalizeOptions(options), { isPool: true }));
|
|
195
197
|
}
|
|
196
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Multi-host pooling (primaries/replicas, failover): named nodes, each
|
|
201
|
+
* backed by a regular pool, selected by glob pattern + 'rr'/'random'/
|
|
202
|
+
* 'order' selector, with connection-failure failover and error-based
|
|
203
|
+
* node offlining. See README § Multi-host pooling.
|
|
204
|
+
*/
|
|
205
|
+
export function poolCluster(options?: PoolClusterOptions): PoolCluster {
|
|
206
|
+
const normalized: PoolClusterOptions = { ...(options || {}) };
|
|
207
|
+
normalized.defaults = normalizeOptions(normalized.defaults || {});
|
|
208
|
+
return new PoolCluster(attach, normalized);
|
|
209
|
+
}
|
|
210
|
+
export type { PoolClusterOptions, ClusterSelector } from './pool-cluster';
|
|
211
|
+
|
|
197
212
|
export { parseConnectionUri, parseConnectionString };
|
|
198
213
|
export { parseNamedPlaceholders } from './named-params';
|
|
199
214
|
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/***************************************
|
|
2
|
+
*
|
|
3
|
+
* PoolCluster — multi-host pooling (primaries/replicas, failover)
|
|
4
|
+
*
|
|
5
|
+
* The mysql2 PoolCluster model on top of this driver's Pool: named
|
|
6
|
+
* nodes, each backed by a regular connection pool (health checks,
|
|
7
|
+
* recycling and metrics included), selected by glob pattern +
|
|
8
|
+
* selector. Consecutive connection failures take a node offline
|
|
9
|
+
* (with optional timed restoration), and get() fails over to the
|
|
10
|
+
* next matching online node.
|
|
11
|
+
*
|
|
12
|
+
***************************************/
|
|
13
|
+
|
|
14
|
+
import Events from 'events';
|
|
15
|
+
import { fromCallback, withPooledConnection } from './callback';
|
|
16
|
+
import type { Callback } from './callback';
|
|
17
|
+
import { parseConnectionString } from './uri';
|
|
18
|
+
import Pool from './pool';
|
|
19
|
+
|
|
20
|
+
type AttachFn = (options: any, callback: Callback) => void;
|
|
21
|
+
|
|
22
|
+
export type ClusterSelector = 'rr' | 'random' | 'order';
|
|
23
|
+
|
|
24
|
+
export interface PoolClusterOptions {
|
|
25
|
+
/** Options shared by every node (user, password, database, …). */
|
|
26
|
+
defaults?: any;
|
|
27
|
+
/** name → per-node option overrides (host, port, …). */
|
|
28
|
+
nodes?: Record<string, any>;
|
|
29
|
+
/** Per-node pool size (default 4). */
|
|
30
|
+
max?: number;
|
|
31
|
+
/** Default selector for get()/of() (default 'rr'). */
|
|
32
|
+
selector?: ClusterSelector;
|
|
33
|
+
/**
|
|
34
|
+
* Consecutive connection failures after which a node goes offline
|
|
35
|
+
* (default 5; 0 disables offlining).
|
|
36
|
+
*/
|
|
37
|
+
removeNodeErrorCount?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Milliseconds after which an offline node is restored and probed
|
|
40
|
+
* again (default 30000; 0 = stay offline until restore()/remove()).
|
|
41
|
+
*/
|
|
42
|
+
restoreNodeTimeout?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface ClusterNode {
|
|
46
|
+
name: string;
|
|
47
|
+
options: any;
|
|
48
|
+
pool: Pool;
|
|
49
|
+
online: boolean;
|
|
50
|
+
errorCount: number;
|
|
51
|
+
restoreTimer: NodeJS.Timeout | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function patternToRegExp(pattern: string): RegExp {
|
|
55
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
56
|
+
return new RegExp('^' + escaped + '$');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Events: 'online' (name) — node restored; 'offline' (name) — node taken
|
|
61
|
+
* out of rotation after too many connection failures; 'remove' (name) —
|
|
62
|
+
* node removed via remove().
|
|
63
|
+
*/
|
|
64
|
+
class PoolCluster extends Events.EventEmitter {
|
|
65
|
+
private attach: AttachFn;
|
|
66
|
+
private nodes = new Map<string, ClusterNode>();
|
|
67
|
+
private rrIndex = new Map<string, number>();
|
|
68
|
+
private max: number;
|
|
69
|
+
private defaults: any;
|
|
70
|
+
private selector: ClusterSelector;
|
|
71
|
+
private removeNodeErrorCount: number;
|
|
72
|
+
private restoreNodeTimeout: number;
|
|
73
|
+
private _destroyed = false;
|
|
74
|
+
|
|
75
|
+
constructor(attach: AttachFn, options?: PoolClusterOptions) {
|
|
76
|
+
super();
|
|
77
|
+
options = options || {};
|
|
78
|
+
this.attach = attach;
|
|
79
|
+
this.defaults = options.defaults || {};
|
|
80
|
+
this.max = options.max && options.max > 0 ? options.max : 4;
|
|
81
|
+
this.selector = options.selector || 'rr';
|
|
82
|
+
this.removeNodeErrorCount = options.removeNodeErrorCount !== undefined ? options.removeNodeErrorCount : 5;
|
|
83
|
+
this.restoreNodeTimeout = options.restoreNodeTimeout !== undefined ? options.restoreNodeTimeout : 30000;
|
|
84
|
+
|
|
85
|
+
for (const [name, overrides] of Object.entries(options.nodes || {})) {
|
|
86
|
+
this.add(name, overrides);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Register a node; its pool is created lazily-safe right away. */
|
|
91
|
+
add(name: string, overrides?: any): this {
|
|
92
|
+
if (this._destroyed) {
|
|
93
|
+
throw new Error('PoolCluster has been destroyed');
|
|
94
|
+
}
|
|
95
|
+
if (this.nodes.has(name)) {
|
|
96
|
+
throw new Error('PoolCluster node already exists: ' + name);
|
|
97
|
+
}
|
|
98
|
+
// a connection-string override must be parsed, not object-spread
|
|
99
|
+
// into character-indexed garbage
|
|
100
|
+
if (typeof overrides === 'string') {
|
|
101
|
+
overrides = parseConnectionString(overrides);
|
|
102
|
+
}
|
|
103
|
+
const nodeOptions = { ...this.defaults, ...(overrides || {}) };
|
|
104
|
+
this.nodes.set(name, {
|
|
105
|
+
name,
|
|
106
|
+
options: nodeOptions,
|
|
107
|
+
pool: new Pool(this.attach, nodeOptions.max || this.max, { ...nodeOptions, isPool: true }),
|
|
108
|
+
online: true,
|
|
109
|
+
errorCount: 0,
|
|
110
|
+
restoreTimer: null,
|
|
111
|
+
});
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Remove a node for good, destroying its pool. */
|
|
116
|
+
remove(name: string, callback?: (err?: any) => void): void {
|
|
117
|
+
const node = this.nodes.get(name);
|
|
118
|
+
if (!node) {
|
|
119
|
+
if (callback) callback();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.nodes.delete(name);
|
|
123
|
+
if (node.restoreTimer) {
|
|
124
|
+
clearTimeout(node.restoreTimer);
|
|
125
|
+
}
|
|
126
|
+
this.emit('remove', name);
|
|
127
|
+
node.pool.destroy(callback);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Bring an offline node back into rotation immediately. */
|
|
131
|
+
restore(name: string): void {
|
|
132
|
+
const node = this.nodes.get(name);
|
|
133
|
+
if (!node || node.online) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (node.restoreTimer) {
|
|
137
|
+
clearTimeout(node.restoreTimer);
|
|
138
|
+
node.restoreTimer = null;
|
|
139
|
+
}
|
|
140
|
+
node.online = true;
|
|
141
|
+
node.errorCount = 0;
|
|
142
|
+
this.emit('online', name);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** name → { online, errorCount, pool metrics } for every node. */
|
|
146
|
+
status(): Record<string, any> {
|
|
147
|
+
const out: Record<string, any> = {};
|
|
148
|
+
for (const node of this.nodes.values()) {
|
|
149
|
+
out[node.name] = {
|
|
150
|
+
online: node.online,
|
|
151
|
+
errorCount: node.errorCount,
|
|
152
|
+
totalCount: node.pool.totalCount,
|
|
153
|
+
idleCount: node.pool.idleCount,
|
|
154
|
+
activeCount: node.pool.activeCount,
|
|
155
|
+
waitingCount: node.pool.waitingCount,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private matching(pattern: string): ClusterNode[] {
|
|
162
|
+
const re = patternToRegExp(pattern);
|
|
163
|
+
const out: ClusterNode[] = [];
|
|
164
|
+
for (const node of this.nodes.values()) {
|
|
165
|
+
if (re.test(node.name)) {
|
|
166
|
+
out.push(node);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private pick(pattern: string, selector: ClusterSelector, exclude: Set<string>): ClusterNode | null {
|
|
173
|
+
const candidates = this.matching(pattern).filter((n) => n.online && !exclude.has(n.name));
|
|
174
|
+
if (!candidates.length) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
if (selector === 'random') {
|
|
178
|
+
return candidates[Math.floor(Math.random() * candidates.length)];
|
|
179
|
+
}
|
|
180
|
+
if (selector === 'order') {
|
|
181
|
+
return candidates[0];
|
|
182
|
+
}
|
|
183
|
+
// round-robin per pattern; only the FIRST pick of a get() advances
|
|
184
|
+
// the counter — failover re-picks reuse it, or a run of failovers
|
|
185
|
+
// would skew the distribution toward nodes after the failing ones
|
|
186
|
+
const index = this.rrIndex.get(pattern) || 0;
|
|
187
|
+
if (exclude.size === 0) {
|
|
188
|
+
this.rrIndex.set(pattern, index + 1);
|
|
189
|
+
}
|
|
190
|
+
return candidates[index % candidates.length];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private noteFailure(node: ClusterNode): void {
|
|
194
|
+
// a node removed while a get was in flight must not accumulate
|
|
195
|
+
// counters, emit 'offline', or arm a restore timer nobody clears
|
|
196
|
+
if (!this.nodes.has(node.name)) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
node.errorCount++;
|
|
200
|
+
if (!this.removeNodeErrorCount || node.errorCount < this.removeNodeErrorCount || !node.online) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
node.online = false;
|
|
204
|
+
this.emit('offline', node.name);
|
|
205
|
+
if (this.restoreNodeTimeout > 0) {
|
|
206
|
+
node.restoreTimer = setTimeout(() => {
|
|
207
|
+
node.restoreTimer = null;
|
|
208
|
+
this.restore(node.name);
|
|
209
|
+
}, this.restoreNodeTimeout);
|
|
210
|
+
if (node.restoreTimer.unref) {
|
|
211
|
+
node.restoreTimer.unref();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Acquire a connection from a node matching `pattern` (default '*').
|
|
218
|
+
* Connection failures mark the node and FAIL OVER to the next
|
|
219
|
+
* matching online node; only when every candidate has failed does the
|
|
220
|
+
* callback receive the last error. Release connections with
|
|
221
|
+
* db.detach(), exactly like a plain pool.
|
|
222
|
+
*/
|
|
223
|
+
get(pattern: string | Callback, selector?: ClusterSelector | Callback, callback?: Callback): void {
|
|
224
|
+
if (typeof pattern === 'function') {
|
|
225
|
+
callback = pattern;
|
|
226
|
+
pattern = '*';
|
|
227
|
+
}
|
|
228
|
+
if (typeof selector === 'function') {
|
|
229
|
+
callback = selector;
|
|
230
|
+
selector = undefined;
|
|
231
|
+
}
|
|
232
|
+
if (this._destroyed) {
|
|
233
|
+
callback!(new Error('PoolCluster has been destroyed'), null);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const sel = (selector as ClusterSelector) || this.selector;
|
|
238
|
+
const tried = new Set<string>();
|
|
239
|
+
const self = this;
|
|
240
|
+
|
|
241
|
+
const attempt = (lastError?: any) => {
|
|
242
|
+
const node = self.pick(pattern as string, sel, tried);
|
|
243
|
+
if (!node) {
|
|
244
|
+
callback!(lastError || new Error('PoolCluster: no online node matches pattern "' + pattern + '"'), null);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
tried.add(node.name);
|
|
248
|
+
node.pool.get((err: any, db: any) => {
|
|
249
|
+
if (err) {
|
|
250
|
+
self.noteFailure(node);
|
|
251
|
+
attempt(err);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
node.errorCount = 0;
|
|
255
|
+
callback!(null, db);
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
attempt();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
getAsync(pattern?: string, selector?: ClusterSelector): Promise<any> {
|
|
262
|
+
const self = this;
|
|
263
|
+
return fromCallback((cb) => self.get(pattern || '*', selector, cb));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* A pool-like facade bound to a pattern (mysql2's cluster.of):
|
|
268
|
+
* { get, getAsync, withConnection } routed through the cluster's
|
|
269
|
+
* selection and failover.
|
|
270
|
+
*/
|
|
271
|
+
of(pattern: string, selector?: ClusterSelector) {
|
|
272
|
+
const self = this;
|
|
273
|
+
return {
|
|
274
|
+
get(callback: Callback) {
|
|
275
|
+
self.get(pattern, selector, callback);
|
|
276
|
+
},
|
|
277
|
+
getAsync() {
|
|
278
|
+
return self.getAsync(pattern, selector);
|
|
279
|
+
},
|
|
280
|
+
withConnection<T>(work: (db: any) => Promise<T> | T): Promise<T> {
|
|
281
|
+
return self.withConnection(pattern, work, selector);
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Run `work` with a connection from a matching node, always released. */
|
|
287
|
+
withConnection<T>(pattern: string, work: (db: any) => Promise<T> | T, selector?: ClusterSelector): Promise<T> {
|
|
288
|
+
return withPooledConnection(() => this.getAsync(pattern, selector), work);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Destroy every node's pool. */
|
|
292
|
+
destroy(callback?: (err?: any) => void): void {
|
|
293
|
+
this._destroyed = true;
|
|
294
|
+
const nodes = [...this.nodes.values()];
|
|
295
|
+
this.nodes.clear();
|
|
296
|
+
let remaining = nodes.length;
|
|
297
|
+
if (!remaining) {
|
|
298
|
+
if (callback) callback();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
let firstError: any = null;
|
|
302
|
+
for (const node of nodes) {
|
|
303
|
+
if (node.restoreTimer) {
|
|
304
|
+
clearTimeout(node.restoreTimer);
|
|
305
|
+
}
|
|
306
|
+
node.pool.destroy((err?: any) => {
|
|
307
|
+
if (err && !firstError) firstError = err;
|
|
308
|
+
if (--remaining === 0 && callback) callback(firstError);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
destroyAsync(): Promise<void> {
|
|
314
|
+
const self = this;
|
|
315
|
+
return fromCallback((cb) => self.destroy(cb));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export default PoolCluster;
|
package/src/pool.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
***************************************/
|
|
6
6
|
|
|
7
7
|
import Events from 'events';
|
|
8
|
-
import { fromCallback } from './callback';
|
|
8
|
+
import { fromCallback, withPooledConnection } from './callback';
|
|
9
9
|
import type { Callback } from './callback';
|
|
10
10
|
|
|
11
11
|
type AttachFn = (options: any, callback: Callback) => void;
|
|
@@ -385,15 +385,8 @@ class Pool extends Events.EventEmitter {
|
|
|
385
385
|
* Run `work` with a connection from the pool, returning it to the pool
|
|
386
386
|
* (detach) when the returned promise settles — success or failure.
|
|
387
387
|
*/
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
try {
|
|
391
|
-
return await work(db);
|
|
392
|
-
} finally {
|
|
393
|
-
// A pooled detach only returns the connection to the pool; do not
|
|
394
|
-
// let a detach hiccup mask the outcome of `work`.
|
|
395
|
-
await new Promise<void>(function(resolve) { db.detach(function() { resolve(); }); });
|
|
396
|
-
}
|
|
388
|
+
withConnection<T>(work: (db: any) => Promise<T> | T): Promise<T> {
|
|
389
|
+
return withPooledConnection(() => this.getAsync(), work);
|
|
397
390
|
}
|
|
398
391
|
}
|
|
399
392
|
|
package/src/wire/xsqlvar.ts
CHANGED
|
@@ -750,8 +750,9 @@ export class SQLVarTimeTz extends SQLVarBase {
|
|
|
750
750
|
data.readInt(); // skip timezone info
|
|
751
751
|
|
|
752
752
|
if (!lowerV13 || !data.readInt()) {
|
|
753
|
+
// the wire value is already UTC; no local-offset adjustment
|
|
753
754
|
var d = new Date(0);
|
|
754
|
-
d.setMilliseconds(Math.floor(time / 10)
|
|
755
|
+
d.setMilliseconds(Math.floor(time / 10));
|
|
755
756
|
return d;
|
|
756
757
|
}
|
|
757
758
|
return null;
|
|
@@ -771,8 +772,9 @@ export class SQLVarTimeTzEx extends SQLVarTimeTz {
|
|
|
771
772
|
data.readInt(); // skip ext_offset
|
|
772
773
|
|
|
773
774
|
if (!lowerV13 || !data.readInt()) {
|
|
775
|
+
// the wire value is already UTC; no local-offset adjustment
|
|
774
776
|
var d = new Date(0);
|
|
775
|
-
d.setMilliseconds(Math.floor(time / 10)
|
|
777
|
+
d.setMilliseconds(Math.floor(time / 10));
|
|
776
778
|
return d;
|
|
777
779
|
}
|
|
778
780
|
return null;
|
|
@@ -792,8 +794,9 @@ export class SQLVarTimeStampTz extends SQLVarBase {
|
|
|
792
794
|
data.readInt(); // skip timezone info
|
|
793
795
|
|
|
794
796
|
if (!lowerV13 || !data.readInt()) {
|
|
797
|
+
// the wire value is already UTC; no local-offset adjustment
|
|
795
798
|
var d = new Date(0);
|
|
796
|
-
d.setMilliseconds((date - DateOffset) * TimeCoeff + Math.floor(time / 10)
|
|
799
|
+
d.setMilliseconds((date - DateOffset) * TimeCoeff + Math.floor(time / 10));
|
|
797
800
|
return d;
|
|
798
801
|
}
|
|
799
802
|
|
|
@@ -815,8 +818,9 @@ export class SQLVarTimeStampTzEx extends SQLVarTimeStampTz {
|
|
|
815
818
|
data.readInt(); // skip ext_offset
|
|
816
819
|
|
|
817
820
|
if (!lowerV13 || !data.readInt()) {
|
|
821
|
+
// the wire value is already UTC; no local-offset adjustment
|
|
818
822
|
var d = new Date(0);
|
|
819
|
-
d.setMilliseconds((date - DateOffset) * TimeCoeff + Math.floor(time / 10)
|
|
823
|
+
d.setMilliseconds((date - DateOffset) * TimeCoeff + Math.floor(time / 10));
|
|
820
824
|
return d;
|
|
821
825
|
}
|
|
822
826
|
|