replica-failover-mongodb-ts 3.0.3 → 3.0.9
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 +106 -7
- package/dist/config/connectionManager.js +214 -78
- package/dist/config/metrics.js +20 -2
- package/dist/index.js +15 -0
- package/dist/nestjs/index.js +18 -0
- package/dist/nestjs/node-balancer.decorators.js +9 -0
- package/dist/nestjs/node-balancer.module.js +65 -0
- package/dist/scripts/test_connection_string.js +58 -0
- package/dist/scripts/test_monitoring.js +46 -0
- package/dist/scripts/test_nestjs_usage.js +131 -0
- package/dist/scripts/test_read_pref.js +87 -0
- package/dist/server.js +2 -1
- package/package.json +13 -2
package/Readme.md
CHANGED
|
@@ -120,7 +120,11 @@ O Node Balancer utiliza as seguintes tecnologias:
|
|
|
120
120
|
|
|
121
121
|
## Uso como Biblioteca (Library)
|
|
122
122
|
|
|
123
|
-
Você pode usar o gerenciador de conexões resiliente deste projeto em sua própria aplicação Node.js.
|
|
123
|
+
Você pode usar o gerenciador de conexões resiliente deste projeto em sua própria aplicação Node.js ou NestJS.
|
|
124
|
+
|
|
125
|
+
### Modo Simples (Recomendado)
|
|
126
|
+
|
|
127
|
+
Basta passar a string de conexão padrão do MongoDB. A lib detecta automaticamente os nós e o banco de dados.
|
|
124
128
|
|
|
125
129
|
1. **Instale a lib:**
|
|
126
130
|
```bash
|
|
@@ -131,18 +135,113 @@ Você pode usar o gerenciador de conexões resiliente deste projeto em sua próp
|
|
|
131
135
|
```typescript
|
|
132
136
|
import { ConnectionManager } from 'replica-failover-mongodb-ts';
|
|
133
137
|
|
|
138
|
+
// ✨ Plug & Play: Apenas a string de conexão!
|
|
134
139
|
const db = new ConnectionManager({
|
|
135
|
-
|
|
136
|
-
'mongodb://mongo1:27017/mydb',
|
|
137
|
-
'mongodb://mongo2:27017/mydb'
|
|
138
|
-
],
|
|
139
|
-
healthCheckIntervalMs: 5000
|
|
140
|
+
connectionString: 'mongodb://mongo1:27017,mongo2:27017/mydb'
|
|
140
141
|
});
|
|
141
142
|
|
|
142
143
|
await db.init();
|
|
143
|
-
|
|
144
|
+
|
|
145
|
+
// ✅ Failover automático para QUALQUER collection
|
|
146
|
+
// Você NÃO precisa configurar as collections antes. Basta usar o nome.
|
|
147
|
+
|
|
148
|
+
// Leitura na collection 'users'
|
|
149
|
+
const users = await db.read('users', c => c.find().toArray());
|
|
150
|
+
|
|
151
|
+
// Escrita na collection 'logs'
|
|
152
|
+
await db.write('logs', c => c.insertOne({ event: 'login' }));
|
|
153
|
+
|
|
154
|
+
// Leitura na collection 'products' com preferência Secundária
|
|
155
|
+
const products = await db.read('products', c => c.find().toArray(), {}, 'secondaryPreferred');
|
|
144
156
|
```
|
|
145
157
|
|
|
158
|
+
const products = await db.read('products', c => c.find().toArray(), {}, 'secondaryPreferred');
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
### 🛰️ Monitoramento e Status (Plug & Play)
|
|
163
|
+
|
|
164
|
+
Você pode verificar a saúde das conexões a qualquer momento ou ouvir eventos em tempo real.
|
|
165
|
+
|
|
166
|
+
**Verificar Status:**
|
|
167
|
+
```typescript
|
|
168
|
+
const status = db.getStatus();
|
|
169
|
+
console.log(status);
|
|
170
|
+
/* Retorno:
|
|
171
|
+
{
|
|
172
|
+
isConnected: true,
|
|
173
|
+
dbName: 'mydb',
|
|
174
|
+
primary: 'mongodb://mongo1:27017/mydb',
|
|
175
|
+
secondaries: ['mongodb://mongo2:27017/mydb'],
|
|
176
|
+
totalNodes: 2
|
|
177
|
+
}
|
|
178
|
+
*/
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Ouvir Eventos (Real-time):**
|
|
182
|
+
A classe `ConnectionManager` emite eventos que você pode escutar:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
db.on('failover-start', (reason) => {
|
|
186
|
+
console.warn('⚠️ O banco principal caiu! Iniciando failover...', reason);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
db.on('failover-complete', ({ newPrimary }) => {
|
|
190
|
+
console.info('✅ Novo banco principal eleito:', newPrimary);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
db.on('node-lost', ({ count }) => {
|
|
194
|
+
console.error('❌ Um nó secundário caiu. Total restante:', count);
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
const db = new ConnectionManager({
|
|
200
|
+
nodes: [
|
|
201
|
+
'mongodb://mongo1:27017/mydb',
|
|
202
|
+
'mongodb://mongo2:27017/mydb'
|
|
203
|
+
],
|
|
204
|
+
healthCheckIntervalMs: 5000,
|
|
205
|
+
minPoolSize: 5
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Uso com NestJS
|
|
210
|
+
|
|
211
|
+
Se você usa NestJS, a integração é nativa:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
// app.module.ts
|
|
215
|
+
import { Module } from '@nestjs/common';
|
|
216
|
+
import { NodeBalancerModule } from 'replica-failover-mongodb-ts/dist/nestjs';
|
|
217
|
+
|
|
218
|
+
@Module({
|
|
219
|
+
imports: [
|
|
220
|
+
NodeBalancerModule.forRoot({
|
|
221
|
+
connectionString: 'mongodb://localhost:27017,localhost:27018/mydb',
|
|
222
|
+
}),
|
|
223
|
+
],
|
|
224
|
+
})
|
|
225
|
+
export class AppModule {}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
E para usar nos seus services:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
import { Injectable } from '@nestjs/common';
|
|
232
|
+
import { InjectConnectionManager } from 'replica-failover-mongodb-ts/dist/nestjs';
|
|
233
|
+
import { ConnectionManager } from 'replica-failover-mongodb-ts';
|
|
234
|
+
|
|
235
|
+
@Injectable()
|
|
236
|
+
export class UserService {
|
|
237
|
+
constructor(@InjectConnectionManager() private readonly db: ConnectionManager) {}
|
|
238
|
+
|
|
239
|
+
async getUsers() {
|
|
240
|
+
return this.db.read('users', (col) => col.find().toArray());
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
146
245
|
---
|
|
147
246
|
|
|
148
247
|
## Visual Dashboard (Painel de Controle)
|
|
@@ -13,37 +13,87 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.ConnectionManager = void 0;
|
|
16
|
+
const events_1 = require("events");
|
|
16
17
|
const mongodb_1 = require("mongodb");
|
|
17
18
|
const axios_1 = __importDefault(require("axios"));
|
|
18
19
|
const logger_1 = require("../middlewares/logger");
|
|
19
20
|
const websocket_1 = require("./websocket");
|
|
20
21
|
const metrics_1 = require("./metrics");
|
|
21
|
-
class ConnectionManager {
|
|
22
|
+
class ConnectionManager extends events_1.EventEmitter {
|
|
22
23
|
constructor(opts) {
|
|
23
|
-
var _a;
|
|
24
|
+
var _a, _b, _c;
|
|
25
|
+
super();
|
|
24
26
|
this.primaryClient = null;
|
|
25
27
|
this.primaryDb = null;
|
|
28
|
+
this.secondaryClients = [];
|
|
29
|
+
this.rrIndex = 0;
|
|
26
30
|
this.nodes = [];
|
|
27
31
|
this.replicaUri = opts.replicaUri;
|
|
28
|
-
this.nodes = (opts.nodes || []).map((u, i) => ({ uri: u, name: `node${i + 1}` }));
|
|
29
|
-
this.dbName = opts.dbName || 'node-balancer';
|
|
30
32
|
this.healthCheckIntervalMs = (_a = opts.healthCheckIntervalMs) !== null && _a !== void 0 ? _a : 5000;
|
|
31
33
|
this.webhookUrl = opts.webhookUrl;
|
|
34
|
+
this.maxPoolSize = (_b = opts.maxPoolSize) !== null && _b !== void 0 ? _b : 20;
|
|
35
|
+
this.minPoolSize = (_c = opts.minPoolSize) !== null && _c !== void 0 ? _c : 1;
|
|
36
|
+
let parsedDbName;
|
|
37
|
+
if (opts.connectionString) {
|
|
38
|
+
parsedDbName = this.parseConnectionString(opts.connectionString);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
this.nodes = (opts.nodes || []).map((u, i) => ({ uri: u, name: `node${i + 1}` }));
|
|
42
|
+
}
|
|
43
|
+
// Use parsed dbName if allowed, or fallback to opts, or default
|
|
44
|
+
this.dbName = opts.dbName || parsedDbName || 'node-balancer';
|
|
45
|
+
}
|
|
46
|
+
parseConnectionString(uri) {
|
|
47
|
+
// Basic Regex to capture: protocol, auth(optional), hosts, db(optional), options(optional)
|
|
48
|
+
const regex = /^(mongodb(?:\+srv)?):\/\/((?:[^@\/]+@)?)?([^/?]+)(?:\/([^?]+))?(?:\?.*)?$/;
|
|
49
|
+
const match = uri.match(regex);
|
|
50
|
+
let foundDbName;
|
|
51
|
+
if (!match) {
|
|
52
|
+
logger_1.logger.warn('Invalid connection string format. Fallback to manual nodes if provided.');
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const protocol = match[1];
|
|
56
|
+
const auth = match[2] || '';
|
|
57
|
+
const hostsPart = match[3];
|
|
58
|
+
const dbPart = match[4];
|
|
59
|
+
if (dbPart)
|
|
60
|
+
foundDbName = dbPart;
|
|
61
|
+
if (protocol === 'mongodb+srv') {
|
|
62
|
+
logger_1.logger.info('Detected SRV connection string. Using as replicaUri.');
|
|
63
|
+
this.replicaUri = uri;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const hosts = hostsPart.split(',');
|
|
67
|
+
this.nodes = hosts.map((h, i) => ({
|
|
68
|
+
uri: `${protocol}://${auth}${h}`,
|
|
69
|
+
name: `node${i + 1}`
|
|
70
|
+
}));
|
|
71
|
+
logger_1.logger.info(`Parsed ${this.nodes.length} nodes from connection string.`);
|
|
72
|
+
}
|
|
73
|
+
return foundDbName;
|
|
32
74
|
}
|
|
33
75
|
init() {
|
|
34
76
|
return __awaiter(this, void 0, void 0, function* () {
|
|
35
77
|
logger_1.logger.info('ConnectionManager: init() starting');
|
|
36
|
-
//
|
|
78
|
+
// 1. Try Replica URI
|
|
37
79
|
if (this.replicaUri) {
|
|
38
80
|
logger_1.logger.info(`Trying replica URI: ${this.replicaUri}`);
|
|
39
81
|
try {
|
|
40
|
-
const c = new mongodb_1.MongoClient(this.replicaUri, {
|
|
82
|
+
const c = new mongodb_1.MongoClient(this.replicaUri, {
|
|
83
|
+
monitorCommands: true,
|
|
84
|
+
minPoolSize: this.minPoolSize,
|
|
85
|
+
maxPoolSize: this.maxPoolSize
|
|
86
|
+
});
|
|
87
|
+
c._uri = this.replicaUri;
|
|
88
|
+
// Attach events BEFORE connecting to capture initial pool creation
|
|
89
|
+
this.attachPoolMonitor(c, 'primary', 'replica-uri');
|
|
41
90
|
yield c.connect();
|
|
42
91
|
const isWritable = yield this.checkWritable(c);
|
|
43
92
|
if (isWritable) {
|
|
44
93
|
logger_1.logger.info('Connected to replicaSet URI and found writable primary.');
|
|
45
94
|
this.attachClient(c);
|
|
46
95
|
this.startHealthChecks();
|
|
96
|
+
this.emit('ready', { mode: 'replica-set', uri: this.replicaUri });
|
|
47
97
|
return;
|
|
48
98
|
}
|
|
49
99
|
else {
|
|
@@ -55,58 +105,102 @@ class ConnectionManager {
|
|
|
55
105
|
logger_1.logger.warn(`Replica URI connect failed: ${err.message}`);
|
|
56
106
|
}
|
|
57
107
|
}
|
|
58
|
-
// Fallback:
|
|
108
|
+
// 2. Fallback: Multi-node Manual Connection
|
|
109
|
+
logger_1.logger.info('Initializing multi-node connection...');
|
|
110
|
+
let connectedCount = 0;
|
|
59
111
|
for (const n of this.nodes) {
|
|
60
|
-
logger_1.logger.info(`
|
|
112
|
+
logger_1.logger.info(`Connecting to node ${n.uri}`);
|
|
61
113
|
try {
|
|
62
|
-
const c = new mongodb_1.MongoClient(n.uri, {
|
|
114
|
+
const c = new mongodb_1.MongoClient(n.uri, {
|
|
115
|
+
directConnection: true,
|
|
116
|
+
monitorCommands: true,
|
|
117
|
+
minPoolSize: this.minPoolSize,
|
|
118
|
+
maxPoolSize: this.maxPoolSize
|
|
119
|
+
});
|
|
120
|
+
c._uri = n.uri;
|
|
121
|
+
this.attachPoolMonitor(c, 'unknown', n.uri);
|
|
63
122
|
yield c.connect();
|
|
123
|
+
connectedCount++;
|
|
64
124
|
const writable = yield this.checkWritable(c);
|
|
65
|
-
if (writable) {
|
|
66
|
-
logger_1.logger.info(`Found
|
|
125
|
+
if (writable && !this.primaryClient) {
|
|
126
|
+
logger_1.logger.info(`Found Primary node at ${n.uri}`);
|
|
67
127
|
this.attachClient(c);
|
|
68
|
-
this.startHealthChecks();
|
|
69
|
-
return;
|
|
70
128
|
}
|
|
71
129
|
else {
|
|
72
|
-
|
|
130
|
+
logger_1.logger.info(`Connected to Secondary node at ${n.uri}`);
|
|
131
|
+
this.secondaryClients.push(c);
|
|
73
132
|
}
|
|
74
133
|
}
|
|
75
134
|
catch (err) {
|
|
76
135
|
logger_1.logger.warn(`Node connect failed ${n.uri}: ${err.message}`);
|
|
77
136
|
}
|
|
78
137
|
}
|
|
79
|
-
|
|
138
|
+
if (connectedCount === 0) {
|
|
139
|
+
throw new Error('No available MongoDB nodes found. Check your cluster.');
|
|
140
|
+
}
|
|
141
|
+
if (!this.primaryClient) {
|
|
142
|
+
logger_1.logger.warn('Initialized without a Primary node! System in Read-Only mode until a node becomes writable.');
|
|
143
|
+
this.emit('warn', 'Initialized in Read-Only mode (No Primary)');
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
this.emit('ready', { mode: 'multi-node' });
|
|
147
|
+
}
|
|
148
|
+
this.startHealthChecks();
|
|
80
149
|
});
|
|
81
150
|
}
|
|
151
|
+
getStatus() {
|
|
152
|
+
return {
|
|
153
|
+
isConnected: !!this.primaryClient,
|
|
154
|
+
dbName: this.dbName,
|
|
155
|
+
primary: this.primaryClient ? this.primaryClient._uri : null,
|
|
156
|
+
secondaries: this.secondaryClients.map(c => c._uri),
|
|
157
|
+
totalNodes: (this.primaryClient ? 1 : 0) + this.secondaryClients.length
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
attachPoolMonitor(client, type, nodeUri) {
|
|
161
|
+
const label = { type, node: nodeUri };
|
|
162
|
+
client.on('connectionCreated', () => metrics_1.poolSize.inc(label));
|
|
163
|
+
client.on('connectionClosed', () => metrics_1.poolSize.dec(label));
|
|
164
|
+
client.on('connectionCheckedOut', () => {
|
|
165
|
+
metrics_1.poolCheckedOut.inc(label);
|
|
166
|
+
});
|
|
167
|
+
client.on('connectionCheckedIn', () => {
|
|
168
|
+
metrics_1.poolCheckedOut.dec(label);
|
|
169
|
+
});
|
|
170
|
+
client.on('connectionCheckOutStarted', () => metrics_1.poolWaitQueue.inc(label));
|
|
171
|
+
client.on('connectionCheckOutFailed', () => metrics_1.poolWaitQueue.dec(label));
|
|
172
|
+
client.on('connectionCheckedOut', () => metrics_1.poolWaitQueue.dec(label));
|
|
173
|
+
}
|
|
82
174
|
attachClient(client) {
|
|
83
175
|
this.primaryClient = client;
|
|
84
176
|
this.primaryDb = client.db(this.dbName);
|
|
85
|
-
|
|
177
|
+
const uri = client._uri;
|
|
178
|
+
this.emit('primary-elected', { uri });
|
|
86
179
|
client.on('topologyDescriptionChanged', (td) => {
|
|
87
180
|
var _a;
|
|
88
|
-
logger_1.logger.info(`topologyDescriptionChanged: ${JSON.stringify(this.summarizeTopology(td))}`);
|
|
89
181
|
const summary = this.summarizeTopology(td);
|
|
90
182
|
this.recordEvent('topologyDescriptionChanged', { td: summary }).catch(() => { });
|
|
91
183
|
(_a = (0, websocket_1.getIO)()) === null || _a === void 0 ? void 0 : _a.emit('topology-change', summary);
|
|
184
|
+
this.emit('topology-change', summary);
|
|
92
185
|
});
|
|
93
186
|
client.on('serverHeartbeatFailed', (event) => {
|
|
94
187
|
logger_1.logger.warn(`serverHeartbeatFailed: ${JSON.stringify(event)}`);
|
|
95
188
|
this.recordEvent('serverHeartbeatFailed', { event }).catch(() => { });
|
|
96
189
|
this.sendAlert('serverHeartbeatFailed', event).catch(() => { });
|
|
190
|
+
this.emit('server-heartbeat-failed', event);
|
|
97
191
|
});
|
|
98
192
|
client.on('serverHeartbeatSucceeded', (event) => {
|
|
99
|
-
|
|
193
|
+
// verbose
|
|
100
194
|
});
|
|
101
195
|
client.on('close', () => {
|
|
102
196
|
logger_1.logger.warn('MongoClient close event');
|
|
103
197
|
this.recordEvent('clientClose', {}).catch(() => { });
|
|
198
|
+
this.emit('close');
|
|
104
199
|
});
|
|
105
200
|
logger_1.logger.info('Primary client attached.');
|
|
106
201
|
metrics_1.connectionStatus.set(1);
|
|
107
202
|
}
|
|
108
203
|
summarizeTopology(td) {
|
|
109
|
-
// gentle summary - driver topologyDescription shape may vary
|
|
110
204
|
try {
|
|
111
205
|
return {
|
|
112
206
|
servers: Object.keys(td.servers || {}).map((k) => ({
|
|
@@ -123,15 +217,14 @@ class ConnectionManager {
|
|
|
123
217
|
return __awaiter(this, void 0, void 0, function* () {
|
|
124
218
|
try {
|
|
125
219
|
const admin = client.db('admin');
|
|
126
|
-
// isWritablePrimary command is supported
|
|
127
220
|
const res = yield admin.command({ isWritablePrimary: 1 }).catch(() => null);
|
|
128
221
|
if (res && res.isWritablePrimary)
|
|
129
222
|
return true;
|
|
130
|
-
// fallback: isMaster / hello
|
|
131
223
|
const info = yield admin.command({ hello: 1 }).catch(() => null);
|
|
132
|
-
if (info && (info.isWritablePrimary ||
|
|
224
|
+
if (info && (info.isWritablePrimary ||
|
|
225
|
+
info.isWritablePrimary === true ||
|
|
226
|
+
info.ismaster === true))
|
|
133
227
|
return true;
|
|
134
|
-
// if driver can't tell, assume writable if connected and write to a temp collection test (careful)
|
|
135
228
|
return false;
|
|
136
229
|
}
|
|
137
230
|
catch (err) {
|
|
@@ -144,19 +237,14 @@ class ConnectionManager {
|
|
|
144
237
|
return __awaiter(this, void 0, void 0, function* () {
|
|
145
238
|
var _a;
|
|
146
239
|
try {
|
|
147
|
-
const event = {
|
|
148
|
-
ts: new Date(),
|
|
149
|
-
level: 'event',
|
|
150
|
-
type,
|
|
151
|
-
payload,
|
|
152
|
-
};
|
|
240
|
+
const event = { ts: new Date(), level: 'event', type, payload };
|
|
153
241
|
(_a = (0, websocket_1.getIO)()) === null || _a === void 0 ? void 0 : _a.emit('log', event);
|
|
154
242
|
if (!this.primaryDb)
|
|
155
243
|
return;
|
|
156
244
|
yield this.primaryDb.collection('logs').insertOne(event);
|
|
157
245
|
}
|
|
158
246
|
catch (err) {
|
|
159
|
-
|
|
247
|
+
// logger.warn('recordEvent failed: ' + (err as Error).message);
|
|
160
248
|
}
|
|
161
249
|
});
|
|
162
250
|
}
|
|
@@ -182,70 +270,111 @@ class ConnectionManager {
|
|
|
182
270
|
}
|
|
183
271
|
healthCheckLoop() {
|
|
184
272
|
return __awaiter(this, void 0, void 0, function* () {
|
|
185
|
-
//
|
|
273
|
+
// 1. Check Primary
|
|
186
274
|
if (this.primaryClient) {
|
|
187
275
|
const ok = yield this.checkWritable(this.primaryClient).catch(() => false);
|
|
188
|
-
if (ok)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
276
|
+
if (!ok) {
|
|
277
|
+
logger_1.logger.warn('Primary client no longer writable. Demoting to potential secondary.');
|
|
278
|
+
this.secondaryClients.push(this.primaryClient);
|
|
279
|
+
this.primaryClient = null;
|
|
280
|
+
this.primaryDb = null;
|
|
281
|
+
metrics_1.connectionStatus.set(0);
|
|
282
|
+
this.emit('failover-start', { reason: 'Primary not writable' });
|
|
193
283
|
}
|
|
194
|
-
catch (_a) { }
|
|
195
|
-
this.primaryClient = null;
|
|
196
|
-
this.primaryDb = null;
|
|
197
|
-
metrics_1.connectionStatus.set(0);
|
|
198
284
|
}
|
|
199
|
-
//
|
|
200
|
-
for (
|
|
285
|
+
// 2. Check Secondaries
|
|
286
|
+
for (let i = this.secondaryClients.length - 1; i >= 0; i--) {
|
|
287
|
+
const sec = this.secondaryClients[i];
|
|
201
288
|
try {
|
|
202
|
-
|
|
203
|
-
yield c.connect();
|
|
204
|
-
const writable = yield this.checkWritable(c);
|
|
205
|
-
if (writable) {
|
|
206
|
-
logger_1.logger.info(`Health-check: promoted ${n.uri} to primary connection`);
|
|
207
|
-
this.attachClient(c);
|
|
208
|
-
yield this.recordEvent('promote', { node: n.uri });
|
|
209
|
-
yield this.sendAlert('promote', { node: n.uri, message: 'Promoted new primary connection' });
|
|
210
|
-
metrics_1.failoverCount.inc();
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
214
|
-
yield c.close();
|
|
215
|
-
}
|
|
289
|
+
yield sec.db('admin').command({ ping: 1 });
|
|
216
290
|
}
|
|
217
291
|
catch (err) {
|
|
218
|
-
logger_1.logger.
|
|
292
|
+
logger_1.logger.warn('Secondary node lost connection. Removing.');
|
|
293
|
+
try {
|
|
294
|
+
yield sec.close();
|
|
295
|
+
}
|
|
296
|
+
catch (_a) { }
|
|
297
|
+
this.secondaryClients.splice(i, 1);
|
|
298
|
+
this.emit('node-lost', { count: this.secondaryClients.length });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// 3. Promote if needed
|
|
302
|
+
if (!this.primaryClient) {
|
|
303
|
+
// logger.warn('No Primary! Searching among secondaries...');
|
|
304
|
+
for (let i = 0; i < this.secondaryClients.length; i++) {
|
|
305
|
+
const client = this.secondaryClients[i];
|
|
306
|
+
const isWritable = yield this.checkWritable(client);
|
|
307
|
+
if (isWritable) {
|
|
308
|
+
logger_1.logger.info('Promoting secondary to Primary!');
|
|
309
|
+
this.attachClient(client);
|
|
310
|
+
this.secondaryClients.splice(i, 1);
|
|
311
|
+
yield this.recordEvent('promote', { message: 'Promoted secondary to primary' });
|
|
312
|
+
yield this.sendAlert('promote', { message: 'Promoted new primary connection' });
|
|
313
|
+
metrics_1.failoverCount.inc();
|
|
314
|
+
this.emit('failover-complete', { newPrimary: client._uri });
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
219
317
|
}
|
|
220
318
|
}
|
|
221
|
-
// no writable found
|
|
222
|
-
logger_1.logger.error('Health-check: no writable nodes found.');
|
|
223
|
-
yield this.recordEvent('no-writable', {});
|
|
224
|
-
yield this.sendAlert('no-writable', { message: 'CRITICAL: No writable nodes found in cluster!' });
|
|
225
319
|
});
|
|
226
320
|
}
|
|
321
|
+
getSecondary() {
|
|
322
|
+
if (this.secondaryClients.length === 0)
|
|
323
|
+
return null;
|
|
324
|
+
const c = this.secondaryClients[this.rrIndex % this.secondaryClients.length];
|
|
325
|
+
this.rrIndex++;
|
|
326
|
+
return c;
|
|
327
|
+
}
|
|
227
328
|
getDb() {
|
|
228
329
|
return this.primaryDb;
|
|
229
330
|
}
|
|
230
|
-
// Generic wrappers that log operations to collection 'logs'
|
|
231
331
|
read(collectionName_1, op_1) {
|
|
232
|
-
return __awaiter(this, arguments, void 0, function* (collectionName, op, meta = {}) {
|
|
233
|
-
|
|
332
|
+
return __awaiter(this, arguments, void 0, function* (collectionName, op, meta = {}, readPref = 'primary') {
|
|
333
|
+
let clientToUse = this.primaryClient;
|
|
334
|
+
let effectivePref = readPref;
|
|
335
|
+
if (readPref === 'secondary') {
|
|
336
|
+
const sec = this.getSecondary();
|
|
337
|
+
if (sec) {
|
|
338
|
+
clientToUse = sec;
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
throw new Error('No secondary node available for read preference "secondary"');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else if (readPref === 'secondaryPreferred') {
|
|
345
|
+
const sec = this.getSecondary();
|
|
346
|
+
if (sec) {
|
|
347
|
+
clientToUse = sec;
|
|
348
|
+
effectivePref = 'secondary';
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
clientToUse = this.primaryClient;
|
|
352
|
+
effectivePref = 'primary';
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (!clientToUse) {
|
|
356
|
+
throw new Error('No active connection available for requested read preference');
|
|
357
|
+
}
|
|
358
|
+
const db = clientToUse.db(this.dbName);
|
|
234
359
|
const start = Date.now();
|
|
235
360
|
try {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const res = yield op(db.collection(collectionName));
|
|
361
|
+
const collection = db.collection(collectionName);
|
|
362
|
+
const res = yield op(collection);
|
|
239
363
|
const took = Date.now() - start;
|
|
240
364
|
yield this.safeLog({
|
|
241
365
|
ts: new Date(),
|
|
242
366
|
op: 'read',
|
|
243
367
|
collection: collectionName,
|
|
244
368
|
success: true,
|
|
245
|
-
meta,
|
|
369
|
+
meta: Object.assign(Object.assign({}, meta), { readPref: effectivePref }),
|
|
246
370
|
durationMs: took,
|
|
247
371
|
});
|
|
248
|
-
metrics_1.operationDuration.observe({
|
|
372
|
+
metrics_1.operationDuration.observe({
|
|
373
|
+
operation: 'read',
|
|
374
|
+
collection: collectionName,
|
|
375
|
+
success: 'true',
|
|
376
|
+
read_preference: effectivePref
|
|
377
|
+
}, took / 1000);
|
|
249
378
|
return res;
|
|
250
379
|
}
|
|
251
380
|
catch (err) {
|
|
@@ -256,10 +385,15 @@ class ConnectionManager {
|
|
|
256
385
|
collection: collectionName,
|
|
257
386
|
success: false,
|
|
258
387
|
error: err.message,
|
|
259
|
-
meta,
|
|
388
|
+
meta: Object.assign(Object.assign({}, meta), { readPref: effectivePref }),
|
|
260
389
|
durationMs: took,
|
|
261
390
|
});
|
|
262
|
-
metrics_1.operationDuration.observe({
|
|
391
|
+
metrics_1.operationDuration.observe({
|
|
392
|
+
operation: 'read',
|
|
393
|
+
collection: collectionName,
|
|
394
|
+
success: 'false',
|
|
395
|
+
read_preference: effectivePref
|
|
396
|
+
}, took / 1000);
|
|
263
397
|
throw err;
|
|
264
398
|
}
|
|
265
399
|
});
|
|
@@ -306,14 +440,13 @@ class ConnectionManager {
|
|
|
306
440
|
try {
|
|
307
441
|
(_a = (0, websocket_1.getIO)()) === null || _a === void 0 ? void 0 : _a.emit('log', doc);
|
|
308
442
|
if (!this.primaryDb) {
|
|
309
|
-
|
|
310
|
-
logger_1.logger.info(JSON.stringify(doc));
|
|
443
|
+
// logger.warn('safeLog: no primaryDb, skipping db log.');
|
|
311
444
|
return;
|
|
312
445
|
}
|
|
313
446
|
yield this.primaryDb.collection('logs').insertOne(doc);
|
|
314
447
|
}
|
|
315
448
|
catch (err) {
|
|
316
|
-
|
|
449
|
+
// silent fail for log
|
|
317
450
|
}
|
|
318
451
|
});
|
|
319
452
|
}
|
|
@@ -321,11 +454,14 @@ class ConnectionManager {
|
|
|
321
454
|
return __awaiter(this, void 0, void 0, function* () {
|
|
322
455
|
if (this.healthInterval)
|
|
323
456
|
clearInterval(this.healthInterval);
|
|
324
|
-
if (this.primaryClient)
|
|
457
|
+
if (this.primaryClient)
|
|
325
458
|
yield this.primaryClient.close().catch(() => { });
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
459
|
+
for (const c of this.secondaryClients)
|
|
460
|
+
yield c.close().catch(() => { });
|
|
461
|
+
this.primaryClient = null;
|
|
462
|
+
this.primaryDb = null;
|
|
463
|
+
this.secondaryClients = [];
|
|
464
|
+
this.removeAllListeners();
|
|
329
465
|
});
|
|
330
466
|
}
|
|
331
467
|
}
|
package/dist/config/metrics.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.operationDuration = exports.failoverCount = exports.connectionStatus = exports.register = void 0;
|
|
6
|
+
exports.poolWaitQueue = exports.poolCheckedOut = exports.poolSize = exports.operationDuration = exports.failoverCount = exports.connectionStatus = exports.register = void 0;
|
|
7
7
|
const prom_client_1 = __importDefault(require("prom-client"));
|
|
8
8
|
// Create a Registry
|
|
9
9
|
exports.register = new prom_client_1.default.Registry();
|
|
@@ -23,7 +23,25 @@ exports.failoverCount = new prom_client_1.default.Counter({
|
|
|
23
23
|
exports.operationDuration = new prom_client_1.default.Histogram({
|
|
24
24
|
name: 'node_balancer_operation_duration_seconds',
|
|
25
25
|
help: 'Duration of database operations in seconds',
|
|
26
|
-
labelNames: ['operation', 'collection', 'success'],
|
|
26
|
+
labelNames: ['operation', 'collection', 'success', 'read_preference'],
|
|
27
27
|
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
|
|
28
28
|
registers: [exports.register]
|
|
29
29
|
});
|
|
30
|
+
exports.poolSize = new prom_client_1.default.Gauge({
|
|
31
|
+
name: 'node_balancer_pool_size',
|
|
32
|
+
help: 'Current size of the connection pool',
|
|
33
|
+
labelNames: ['type', 'node'],
|
|
34
|
+
registers: [exports.register]
|
|
35
|
+
});
|
|
36
|
+
exports.poolCheckedOut = new prom_client_1.default.Gauge({
|
|
37
|
+
name: 'node_balancer_pool_checked_out',
|
|
38
|
+
help: 'Number of connections currently checked out',
|
|
39
|
+
labelNames: ['type', 'node'],
|
|
40
|
+
registers: [exports.register]
|
|
41
|
+
});
|
|
42
|
+
exports.poolWaitQueue = new prom_client_1.default.Gauge({
|
|
43
|
+
name: 'node_balancer_pool_wait_queue',
|
|
44
|
+
help: 'Number of requests waiting for a connection',
|
|
45
|
+
labelNames: ['type', 'node'],
|
|
46
|
+
registers: [exports.register]
|
|
47
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
2
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
17
|
exports.ConnectionManager = void 0;
|
|
4
18
|
var connectionManager_1 = require("./config/connectionManager");
|
|
5
19
|
Object.defineProperty(exports, "ConnectionManager", { enumerable: true, get: function () { return connectionManager_1.ConnectionManager; } });
|
|
20
|
+
__exportStar(require("./nestjs"), exports);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./node-balancer.module"), exports);
|
|
18
|
+
__exportStar(require("./node-balancer.decorators"), exports);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NODE_BALANCER_CONNECTION = void 0;
|
|
4
|
+
exports.InjectConnectionManager = InjectConnectionManager;
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
exports.NODE_BALANCER_CONNECTION = 'NODE_BALANCER_CONNECTION';
|
|
7
|
+
function InjectConnectionManager() {
|
|
8
|
+
return (0, common_1.Inject)(exports.NODE_BALANCER_CONNECTION);
|
|
9
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
9
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
10
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
11
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
12
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
13
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
14
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var NodeBalancerModule_1;
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.NodeBalancerModule = void 0;
|
|
20
|
+
const common_1 = require("@nestjs/common");
|
|
21
|
+
const connectionManager_1 = require("../config/connectionManager");
|
|
22
|
+
const node_balancer_decorators_1 = require("./node-balancer.decorators");
|
|
23
|
+
let NodeBalancerModule = NodeBalancerModule_1 = class NodeBalancerModule {
|
|
24
|
+
static forRoot(options) {
|
|
25
|
+
const connectionProvider = {
|
|
26
|
+
provide: node_balancer_decorators_1.NODE_BALANCER_CONNECTION,
|
|
27
|
+
useFactory: () => __awaiter(this, void 0, void 0, function* () {
|
|
28
|
+
console.log('DEBUG: NodeBalancerModule useFactory called');
|
|
29
|
+
const manager = new connectionManager_1.ConnectionManager(options);
|
|
30
|
+
console.log('DEBUG: Manager created, initializing...');
|
|
31
|
+
yield manager.init();
|
|
32
|
+
console.log('DEBUG: Manager initialized successfully');
|
|
33
|
+
return manager;
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
module: NodeBalancerModule_1,
|
|
38
|
+
providers: [connectionProvider],
|
|
39
|
+
exports: [connectionProvider],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
static forRootAsync(options) {
|
|
43
|
+
const connectionProvider = {
|
|
44
|
+
provide: node_balancer_decorators_1.NODE_BALANCER_CONNECTION,
|
|
45
|
+
useFactory: (...args) => __awaiter(this, void 0, void 0, function* () {
|
|
46
|
+
const config = yield options.useFactory(...args);
|
|
47
|
+
const manager = new connectionManager_1.ConnectionManager(config);
|
|
48
|
+
yield manager.init();
|
|
49
|
+
return manager;
|
|
50
|
+
}),
|
|
51
|
+
inject: options.inject || [],
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
module: NodeBalancerModule_1,
|
|
55
|
+
imports: options.imports || [],
|
|
56
|
+
providers: [connectionProvider],
|
|
57
|
+
exports: [connectionProvider],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
exports.NodeBalancerModule = NodeBalancerModule;
|
|
62
|
+
exports.NodeBalancerModule = NodeBalancerModule = NodeBalancerModule_1 = __decorate([
|
|
63
|
+
(0, common_1.Global)(),
|
|
64
|
+
(0, common_1.Module)({})
|
|
65
|
+
], NodeBalancerModule);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const connectionManager_1 = require("../config/connectionManager");
|
|
13
|
+
function testConnectionString() {
|
|
14
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
15
|
+
console.log('--- Testing Single Connection String Support ---');
|
|
16
|
+
// Test 1: Standard Multi-node string
|
|
17
|
+
const uri = 'mongodb://localhost:27017,localhost:27018/test_db_string';
|
|
18
|
+
console.log(`Testing URI: ${uri}`);
|
|
19
|
+
const cm = new connectionManager_1.ConnectionManager({
|
|
20
|
+
connectionString: uri,
|
|
21
|
+
minPoolSize: 1,
|
|
22
|
+
maxPoolSize: 2
|
|
23
|
+
});
|
|
24
|
+
// internal check (using any to bypass private check for test)
|
|
25
|
+
const nodes = cm.nodes;
|
|
26
|
+
console.log('Parsed Nodes:', nodes);
|
|
27
|
+
const dbName = cm.dbName;
|
|
28
|
+
console.log('Parsed DB Name:', dbName);
|
|
29
|
+
if (nodes.length !== 2)
|
|
30
|
+
console.error('❌ Incorrect node count parsed');
|
|
31
|
+
else
|
|
32
|
+
console.log('✅ Node count correct');
|
|
33
|
+
if (dbName !== 'test_db_string')
|
|
34
|
+
console.error('❌ Incorrect DB name parsed');
|
|
35
|
+
else
|
|
36
|
+
console.log('✅ DB name correct');
|
|
37
|
+
try {
|
|
38
|
+
console.log('Attempting connection (might fail if no DB)...');
|
|
39
|
+
yield cm.init();
|
|
40
|
+
console.log('✅ Init success with connection string');
|
|
41
|
+
const db = cm.getDb();
|
|
42
|
+
if (db) {
|
|
43
|
+
console.log('✅ DB Connection active');
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.warn('⚠️ No active DB (might be no primary available, but parse worked)');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
console.warn('⚠️ Connection failed (expected if DB is down):', err.message);
|
|
51
|
+
console.log('✅ Parsing logic verified independently of connection.');
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
yield cm.close();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
testConnectionString();
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const connectionManager_1 = require("../config/connectionManager");
|
|
13
|
+
function testMonitoring() {
|
|
14
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
15
|
+
console.log('--- Testing Monitoring & Events ---');
|
|
16
|
+
const uri = 'mongodb://localhost:27017,localhost:27018/test_monitor_db';
|
|
17
|
+
const cm = new connectionManager_1.ConnectionManager({
|
|
18
|
+
connectionString: uri,
|
|
19
|
+
minPoolSize: 1
|
|
20
|
+
});
|
|
21
|
+
// Check Initial Status
|
|
22
|
+
console.log('Initial Status:', cm.getStatus());
|
|
23
|
+
// Listen to Events
|
|
24
|
+
cm.on('ready', (info) => {
|
|
25
|
+
console.log('✅ EVENT: ready', info);
|
|
26
|
+
});
|
|
27
|
+
cm.on('primary-elected', (info) => {
|
|
28
|
+
console.log('✅ EVENT: primary-elected', info);
|
|
29
|
+
});
|
|
30
|
+
try {
|
|
31
|
+
console.log('Initializing...');
|
|
32
|
+
// We expect this to fail connecting if DB is down, but 'warn' event might emit?
|
|
33
|
+
// Or if it connects, 'ready' emits.
|
|
34
|
+
yield cm.init();
|
|
35
|
+
console.log('Post-Init Status:', cm.getStatus());
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
console.warn('⚠️ Connection failed (expected if DB is down). Checking status anyway...');
|
|
39
|
+
console.log('Final Status:', cm.getStatus());
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
yield cm.close();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
testMonitoring();
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
15
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
16
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
17
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
18
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
19
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
20
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
require("reflect-metadata");
|
|
25
|
+
const common_1 = require("@nestjs/common");
|
|
26
|
+
const core_1 = require("@nestjs/core");
|
|
27
|
+
const node_balancer_module_1 = require("../nestjs/node-balancer.module");
|
|
28
|
+
const node_balancer_decorators_1 = require("../nestjs/node-balancer.decorators");
|
|
29
|
+
const connectionManager_1 = require("../config/connectionManager");
|
|
30
|
+
process.on('unhandledRejection', (reason, p) => {
|
|
31
|
+
console.error('Unhandled Rejection at:', p, 'reason:', reason);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
process.on('uncaughtException', (err) => {
|
|
35
|
+
console.error('Uncaught Exception:', err);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
});
|
|
38
|
+
let AppService = class AppService {
|
|
39
|
+
constructor(connectionManager) {
|
|
40
|
+
this.connectionManager = connectionManager;
|
|
41
|
+
}
|
|
42
|
+
check() {
|
|
43
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
44
|
+
if (this.connectionManager) {
|
|
45
|
+
console.log('✅ ConnectionManager injected successfully!');
|
|
46
|
+
const db = this.connectionManager.getDb();
|
|
47
|
+
console.log(`✅ Database context available: ${db ? 'Yes' : 'No (init might be pending or failed)'}`);
|
|
48
|
+
return 'ok';
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.error('❌ ConnectionManager failed to inject.');
|
|
52
|
+
return 'fail';
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
AppService = __decorate([
|
|
58
|
+
(0, common_1.Injectable)(),
|
|
59
|
+
__param(0, (0, node_balancer_decorators_1.InjectConnectionManager)()),
|
|
60
|
+
__metadata("design:paramtypes", [connectionManager_1.ConnectionManager])
|
|
61
|
+
], AppService);
|
|
62
|
+
let AppController = class AppController {
|
|
63
|
+
constructor(appService) {
|
|
64
|
+
this.appService = appService;
|
|
65
|
+
}
|
|
66
|
+
getHello() {
|
|
67
|
+
return this.appService.check();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
__decorate([
|
|
71
|
+
(0, common_1.Get)(),
|
|
72
|
+
__metadata("design:type", Function),
|
|
73
|
+
__metadata("design:paramtypes", []),
|
|
74
|
+
__metadata("design:returntype", void 0)
|
|
75
|
+
], AppController.prototype, "getHello", null);
|
|
76
|
+
AppController = __decorate([
|
|
77
|
+
(0, common_1.Controller)(),
|
|
78
|
+
__metadata("design:paramtypes", [AppService])
|
|
79
|
+
], AppController);
|
|
80
|
+
let AppModule = class AppModule {
|
|
81
|
+
};
|
|
82
|
+
AppModule = __decorate([
|
|
83
|
+
(0, common_1.Module)({
|
|
84
|
+
imports: [
|
|
85
|
+
node_balancer_module_1.NodeBalancerModule.forRoot({
|
|
86
|
+
nodes: ['mongodb://localhost:27017', 'mongodb://localhost:27018', 'mongodb://localhost:27019'],
|
|
87
|
+
minPoolSize: 1,
|
|
88
|
+
maxPoolSize: 5,
|
|
89
|
+
dbName: 'test_nest'
|
|
90
|
+
})
|
|
91
|
+
],
|
|
92
|
+
controllers: [AppController],
|
|
93
|
+
providers: [AppService],
|
|
94
|
+
})
|
|
95
|
+
], AppModule);
|
|
96
|
+
function manualTest() {
|
|
97
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
98
|
+
console.log('DEBUG: Starting manual test...');
|
|
99
|
+
try {
|
|
100
|
+
const m = new connectionManager_1.ConnectionManager({
|
|
101
|
+
nodes: ['mongodb://localhost:27017', 'mongodb://localhost:27018', 'mongodb://localhost:27019'],
|
|
102
|
+
minPoolSize: 1,
|
|
103
|
+
maxPoolSize: 5
|
|
104
|
+
});
|
|
105
|
+
yield m.init();
|
|
106
|
+
console.log('DEBUG: Manual test passed');
|
|
107
|
+
// await m.close(); // Keep it open or close? Close.
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
console.error('DEBUG: Manual test failed', e);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function bootstrap() {
|
|
115
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
116
|
+
console.log('🚀 Starting NestJS Context...');
|
|
117
|
+
yield manualTest();
|
|
118
|
+
try {
|
|
119
|
+
const app = yield core_1.NestFactory.createApplicationContext(AppModule, { logger: ['error', 'warn', 'debug', 'verbose'] });
|
|
120
|
+
const service = app.get(AppService);
|
|
121
|
+
yield service.check();
|
|
122
|
+
yield app.close();
|
|
123
|
+
console.log('✅ NestJS Test Finished.');
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
console.error('❌ NestJS Boot failed:', error);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
bootstrap();
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const connectionManager_1 = require("../config/connectionManager");
|
|
16
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
17
|
+
dotenv_1.default.config({ path: '.env.local' });
|
|
18
|
+
function run() {
|
|
19
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20
|
+
const envNodes = process.env.MONGODB_NODES || '';
|
|
21
|
+
const nodes = envNodes ? envNodes.split(',') : [
|
|
22
|
+
'mongodb://localhost:27017',
|
|
23
|
+
'mongodb://localhost:27018',
|
|
24
|
+
'mongodb://localhost:27019'
|
|
25
|
+
];
|
|
26
|
+
console.log('--- Testing Read Preference and Multi-Node support ---');
|
|
27
|
+
console.log(`Nodes: ${nodes.join(', ')}`);
|
|
28
|
+
const db = new connectionManager_1.ConnectionManager({
|
|
29
|
+
nodes,
|
|
30
|
+
healthCheckIntervalMs: 2000,
|
|
31
|
+
maxPoolSize: 5,
|
|
32
|
+
minPoolSize: 1
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
yield db.init();
|
|
36
|
+
console.log('✅ Initialization complete.');
|
|
37
|
+
// Test Write (Primary)
|
|
38
|
+
console.log('📝 Testing Write (Primary)...');
|
|
39
|
+
try {
|
|
40
|
+
yield db.write('test_reads', (col) => __awaiter(this, void 0, void 0, function* () {
|
|
41
|
+
yield col.insertOne({ test: 'read_pref', date: new Date() });
|
|
42
|
+
}));
|
|
43
|
+
console.log('✅ Write successful.');
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
console.warn('⚠️ Write failed (No Primary?), skipping to read tests... Error:', err.message);
|
|
47
|
+
}
|
|
48
|
+
// Test Read (Primary default)
|
|
49
|
+
console.log('📖 Testing Read (Primary Default)...');
|
|
50
|
+
try {
|
|
51
|
+
const res1 = yield db.read('test_reads', (col) => __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
return col.findOne({ test: 'read_pref' });
|
|
53
|
+
}));
|
|
54
|
+
console.log('✅ Read Primary result:', res1 === null || res1 === void 0 ? void 0 : res1._id);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
console.warn('⚠️ Read Primary failed (No Primary?), skipping... Error:', err.message);
|
|
58
|
+
}
|
|
59
|
+
// Test Read (Secondary Strict)
|
|
60
|
+
console.log('📖 Testing Read (Secondary Strict)...');
|
|
61
|
+
try {
|
|
62
|
+
const res2 = yield db.read('test_reads', (col) => __awaiter(this, void 0, void 0, function* () {
|
|
63
|
+
return col.findOne({ test: 'read_pref' });
|
|
64
|
+
}), {}, 'secondary'); // <--- requesting secondary
|
|
65
|
+
console.log('✅ Read Secondary result:', res2 === null || res2 === void 0 ? void 0 : res2._id);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
console.error('❌ Read Secondary failed:', err.message);
|
|
69
|
+
}
|
|
70
|
+
// Test Read (Secondary Preferred)
|
|
71
|
+
console.log('📖 Testing Read (Secondary Preferred)...');
|
|
72
|
+
const res3 = yield db.read('test_reads', (col) => __awaiter(this, void 0, void 0, function* () {
|
|
73
|
+
return col.findOne({ test: 'read_pref' });
|
|
74
|
+
}), {}, 'secondaryPreferred');
|
|
75
|
+
console.log('✅ Read SecondaryPreferred result:', res3 === null || res3 === void 0 ? void 0 : res3._id);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
console.error('❌ Test failed:', err);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
yield db.close();
|
|
82
|
+
console.log('👋 Connection closed.');
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
run();
|
package/dist/server.js
CHANGED
|
@@ -23,7 +23,8 @@ app_1.default.get('/metrics', (req, res) => __awaiter(void 0, void 0, void 0, fu
|
|
|
23
23
|
res.end(yield metrics_1.register.metrics());
|
|
24
24
|
}
|
|
25
25
|
catch (ex) {
|
|
26
|
-
|
|
26
|
+
logger_1.logger.error('Error while serving /metrics endpoint', ex);
|
|
27
|
+
res.status(500).end('Internal server error');
|
|
27
28
|
}
|
|
28
29
|
}));
|
|
29
30
|
const server = app_1.default.listen(PORT, () => {
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "replica-failover-mongodb-ts",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.9",
|
|
4
4
|
"description": "O sistema replica um dos conceitos básicos da arquitetura de sistemas distribuído, na qual ele tem uma API em express.js com TypeScript, e usando o mongoDB com replicaSET e failover, consegue automáticamente garantir disponibilidade e redundância de dados das requisições.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
+
"test": "node dist/scripts/test_read_pref.js",
|
|
8
9
|
"dev": "nodemon --exec ts-node src/server.ts",
|
|
9
10
|
"build": "tsc",
|
|
10
11
|
"start": "node dist/server.js",
|
|
@@ -29,6 +30,11 @@
|
|
|
29
30
|
"keywords": [],
|
|
30
31
|
"author": "",
|
|
31
32
|
"license": "ISC",
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
|
35
|
+
"@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
|
36
|
+
"rxjs": "^7.0.0"
|
|
37
|
+
},
|
|
32
38
|
"dependencies": {
|
|
33
39
|
"@types/blessed": "^0.1.26",
|
|
34
40
|
"@types/inquirer": "^9.0.9",
|
|
@@ -46,11 +52,16 @@
|
|
|
46
52
|
"winston": "^3.18.3"
|
|
47
53
|
},
|
|
48
54
|
"devDependencies": {
|
|
55
|
+
"@nestjs/common": "^11.1.13",
|
|
56
|
+
"@nestjs/core": "^11.1.13",
|
|
57
|
+
"@nestjs/platform-express": "^11.1.13",
|
|
49
58
|
"@types/express": "^5.0.3",
|
|
50
59
|
"@types/morgan": "^1.9.10",
|
|
51
60
|
"@types/node": "^24.8.0",
|
|
52
61
|
"nodemon": "^3.1.10",
|
|
62
|
+
"reflect-metadata": "^0.2.2",
|
|
63
|
+
"rxjs": "^7.8.2",
|
|
53
64
|
"ts-node": "^10.9.2",
|
|
54
|
-
"typescript": "^5.
|
|
65
|
+
"typescript": "^5.6.3"
|
|
55
66
|
}
|
|
56
67
|
}
|