@joystick.js/db-canary 0.0.0-canary.2275 → 0.0.0-canary.2276
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 +87 -104
- package/debug_test_runner.js +208 -0
- package/dist/server/index.js +1 -1
- package/dist/server/lib/operation_dispatcher.js +1 -1
- package/dist/server/lib/operations/admin.js +1 -1
- package/dist/server/lib/simple_sync_manager.js +1 -0
- package/dist/server/lib/sync_receiver.js +1 -0
- package/full_debug_test_runner.js +197 -0
- package/package.json +2 -2
- package/src/server/index.js +25 -24
- package/src/server/lib/operation_dispatcher.js +16 -10
- package/src/server/lib/operations/admin.js +64 -31
- package/src/server/lib/simple_sync_manager.js +444 -0
- package/src/server/lib/sync_receiver.js +461 -0
- package/tests/server/lib/simple_sync_system.test.js +124 -0
- package/dist/server/lib/replication_manager.js +0 -1
- package/dist/server/lib/write_forwarder.js +0 -1
- package/src/server/lib/replication_manager.js +0 -727
- package/src/server/lib/write_forwarder.js +0 -636
- package/tests/server/lib/replication_manager.test.js +0 -202
- package/tests/server/lib/write_forwarder.test.js +0 -258
|
@@ -1,727 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Database replication manager for JoystickDB master-secondary architecture.
|
|
3
|
-
* Provides asynchronous replication of write operations to secondary nodes with authentication,
|
|
4
|
-
* batching, retry logic, and comprehensive monitoring. Supports dynamic secondary management,
|
|
5
|
-
* health checking, and graceful failover handling with detailed statistics tracking.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import net from 'net';
|
|
9
|
-
import crypto from 'crypto';
|
|
10
|
-
import { get_settings } from './load_settings.js';
|
|
11
|
-
import { encode_message, create_message_parser } from './tcp_protocol.js';
|
|
12
|
-
import create_logger from './logger.js';
|
|
13
|
-
|
|
14
|
-
const { create_context_logger } = create_logger('replication');
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Manages database replication to secondary nodes with comprehensive monitoring and control.
|
|
18
|
-
* Handles connection management, authentication, operation queuing, batching, and statistics.
|
|
19
|
-
* Provides both asynchronous and synchronous replication modes with configurable parameters.
|
|
20
|
-
*/
|
|
21
|
-
class ReplicationManager {
|
|
22
|
-
/**
|
|
23
|
-
* Creates a new ReplicationManager instance with default configuration.
|
|
24
|
-
* Initializes connection tracking, operation queuing, and statistics collection.
|
|
25
|
-
*/
|
|
26
|
-
constructor() {
|
|
27
|
-
/** @type {Map<string, Object>} Map of secondary node connections by ID */
|
|
28
|
-
this.secondary_connections = new Map();
|
|
29
|
-
|
|
30
|
-
/** @type {Array<Object>} Queue of operations pending replication */
|
|
31
|
-
this.replication_queue = [];
|
|
32
|
-
|
|
33
|
-
/** @type {boolean} Flag indicating if replication processing is active */
|
|
34
|
-
this.processing_replication = false;
|
|
35
|
-
|
|
36
|
-
/** @type {number} Monotonic sequence number for operation ordering */
|
|
37
|
-
this.sequence_number = 0;
|
|
38
|
-
|
|
39
|
-
/** @type {boolean} Whether replication is enabled */
|
|
40
|
-
this.enabled = false;
|
|
41
|
-
|
|
42
|
-
/** @type {string} Replication mode ('async' or 'sync') */
|
|
43
|
-
this.mode = 'async';
|
|
44
|
-
|
|
45
|
-
/** @type {number} Timeout for replication operations in milliseconds */
|
|
46
|
-
this.timeout_ms = 5000;
|
|
47
|
-
|
|
48
|
-
/** @type {number} Number of retry attempts for failed operations */
|
|
49
|
-
this.retry_attempts = 3;
|
|
50
|
-
|
|
51
|
-
/** @type {number} Maximum operations per replication batch */
|
|
52
|
-
this.batch_size = 100;
|
|
53
|
-
|
|
54
|
-
/** @type {Object} Logger instance for replication events */
|
|
55
|
-
this.log = create_context_logger();
|
|
56
|
-
|
|
57
|
-
/** @type {Object} Replication statistics and metrics */
|
|
58
|
-
this.stats = {
|
|
59
|
-
total_operations_replicated: 0,
|
|
60
|
-
successful_replications: 0,
|
|
61
|
-
failed_replications: 0,
|
|
62
|
-
connected_secondaries: 0,
|
|
63
|
-
avg_replication_latency_ms: 0,
|
|
64
|
-
total_replication_time_ms: 0
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Initializes replication manager with configuration from settings.
|
|
70
|
-
* Loads replication configuration, connects to secondary nodes, and starts processing.
|
|
71
|
-
* Gracefully handles missing or invalid configuration by disabling replication.
|
|
72
|
-
* @returns {void}
|
|
73
|
-
*/
|
|
74
|
-
initialize() {
|
|
75
|
-
try {
|
|
76
|
-
const settings = get_settings();
|
|
77
|
-
const replication_config = settings.replication;
|
|
78
|
-
|
|
79
|
-
if (!replication_config || !replication_config.enabled) {
|
|
80
|
-
this.log.info('Replication disabled in settings');
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
this.enabled = replication_config.enabled;
|
|
85
|
-
this.mode = replication_config.mode || 'async';
|
|
86
|
-
this.timeout_ms = replication_config.timeout_ms || 5000;
|
|
87
|
-
this.retry_attempts = replication_config.retry_attempts || 3;
|
|
88
|
-
this.batch_size = replication_config.batch_size || 100;
|
|
89
|
-
|
|
90
|
-
const secondaries = replication_config.secondaries || [];
|
|
91
|
-
|
|
92
|
-
this.log.info('Initializing replication manager', {
|
|
93
|
-
enabled: this.enabled,
|
|
94
|
-
mode: this.mode,
|
|
95
|
-
secondary_count: secondaries.length,
|
|
96
|
-
timeout_ms: this.timeout_ms,
|
|
97
|
-
batch_size: this.batch_size
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// Connect to all enabled secondary nodes
|
|
101
|
-
for (const secondary of secondaries) {
|
|
102
|
-
if (secondary.enabled) {
|
|
103
|
-
this.connect_to_secondary(secondary);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Start replication processing
|
|
108
|
-
this.start_replication_processing();
|
|
109
|
-
|
|
110
|
-
} catch (error) {
|
|
111
|
-
this.log.warn('Could not initialize replication - settings not loaded', {
|
|
112
|
-
error: error.message
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Establishes connection to a secondary node with authentication and error handling.
|
|
119
|
-
* Sets up TCP connection, message parsing, event handlers, and automatic reconnection.
|
|
120
|
-
* Tracks connection state and updates statistics for monitoring purposes.
|
|
121
|
-
* @param {Object} secondary - Secondary node configuration
|
|
122
|
-
* @param {string} secondary.id - Unique identifier for the secondary node
|
|
123
|
-
* @param {string} secondary.ip - IP address of the secondary node
|
|
124
|
-
* @param {number} secondary.port - Port number of the secondary node
|
|
125
|
-
* @param {string} secondary.private_key - Base64 encoded private key for authentication
|
|
126
|
-
* @returns {Promise<void>}
|
|
127
|
-
*/
|
|
128
|
-
async connect_to_secondary(secondary) {
|
|
129
|
-
const { id, ip, port, private_key } = secondary;
|
|
130
|
-
|
|
131
|
-
this.log.info('Connecting to secondary node', { id, ip, port });
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
const socket = new net.Socket();
|
|
135
|
-
const message_parser = create_message_parser();
|
|
136
|
-
|
|
137
|
-
socket.connect(port, ip, () => {
|
|
138
|
-
this.log.info('Connected to secondary node', { id, ip, port });
|
|
139
|
-
|
|
140
|
-
// Send authentication message
|
|
141
|
-
this.authenticate_with_secondary(socket, id, private_key);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
socket.on('data', (data) => {
|
|
145
|
-
try {
|
|
146
|
-
const messages = message_parser.parse_messages(data);
|
|
147
|
-
for (const message of messages) {
|
|
148
|
-
this.handle_secondary_response(id, message);
|
|
149
|
-
}
|
|
150
|
-
} catch (error) {
|
|
151
|
-
this.log.error('Failed to parse secondary response', {
|
|
152
|
-
secondary_id: id,
|
|
153
|
-
error: error.message
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
socket.on('error', (error) => {
|
|
159
|
-
this.log.error('Secondary connection error', {
|
|
160
|
-
secondary_id: id,
|
|
161
|
-
error: error.message
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
this.secondary_connections.delete(id);
|
|
165
|
-
this.stats.connected_secondaries = this.secondary_connections.size;
|
|
166
|
-
|
|
167
|
-
// Attempt reconnection after delay
|
|
168
|
-
setTimeout(() => {
|
|
169
|
-
this.connect_to_secondary(secondary);
|
|
170
|
-
}, 5000);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
socket.on('close', () => {
|
|
174
|
-
this.log.warn('Secondary connection closed', { secondary_id: id });
|
|
175
|
-
this.secondary_connections.delete(id);
|
|
176
|
-
this.stats.connected_secondaries = this.secondary_connections.size;
|
|
177
|
-
|
|
178
|
-
// Attempt reconnection after delay
|
|
179
|
-
setTimeout(() => {
|
|
180
|
-
this.connect_to_secondary(secondary);
|
|
181
|
-
}, 5000);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
this.secondary_connections.set(id, {
|
|
185
|
-
socket,
|
|
186
|
-
id,
|
|
187
|
-
ip,
|
|
188
|
-
port,
|
|
189
|
-
authenticated: false,
|
|
190
|
-
last_ping: Date.now(),
|
|
191
|
-
pending_operations: new Map()
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
this.stats.connected_secondaries = this.secondary_connections.size;
|
|
195
|
-
|
|
196
|
-
} catch (error) {
|
|
197
|
-
this.log.error('Failed to connect to secondary', {
|
|
198
|
-
secondary_id: id,
|
|
199
|
-
ip,
|
|
200
|
-
port,
|
|
201
|
-
error: error.message
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Authenticates with a secondary node using HMAC-SHA256 signature.
|
|
208
|
-
* Creates timestamped signature using private key and sends authentication message.
|
|
209
|
-
* Provides secure authentication to prevent unauthorized replication access.
|
|
210
|
-
* @param {net.Socket} socket - Socket connection to secondary
|
|
211
|
-
* @param {string} secondary_id - Secondary node ID
|
|
212
|
-
* @param {string} private_key - Base64 encoded private key for signing
|
|
213
|
-
* @returns {void}
|
|
214
|
-
*/
|
|
215
|
-
authenticate_with_secondary(socket, secondary_id, private_key) {
|
|
216
|
-
try {
|
|
217
|
-
const timestamp = Date.now();
|
|
218
|
-
const message_to_sign = `${secondary_id}:${timestamp}`;
|
|
219
|
-
|
|
220
|
-
// Create signature using private key
|
|
221
|
-
const signature = crypto
|
|
222
|
-
.createHmac('sha256', Buffer.from(private_key, 'base64'))
|
|
223
|
-
.update(message_to_sign)
|
|
224
|
-
.digest('base64');
|
|
225
|
-
|
|
226
|
-
const auth_message = {
|
|
227
|
-
type: 'replication_auth',
|
|
228
|
-
node_id: secondary_id,
|
|
229
|
-
timestamp,
|
|
230
|
-
signature
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
const encoded_message = encode_message(auth_message);
|
|
234
|
-
socket.write(encoded_message);
|
|
235
|
-
|
|
236
|
-
this.log.debug('Sent authentication to secondary', { secondary_id });
|
|
237
|
-
|
|
238
|
-
} catch (error) {
|
|
239
|
-
this.log.error('Failed to authenticate with secondary', {
|
|
240
|
-
secondary_id,
|
|
241
|
-
error: error.message
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Processes responses from secondary nodes including authentication and acknowledgments.
|
|
248
|
-
* Routes different message types to appropriate handlers and maintains connection state.
|
|
249
|
-
* Handles authentication results, replication acknowledgments, and ping responses.
|
|
250
|
-
* @param {string} secondary_id - Secondary node ID that sent the response
|
|
251
|
-
* @param {Object|string} message - Parsed message from secondary node
|
|
252
|
-
* @returns {void}
|
|
253
|
-
*/
|
|
254
|
-
handle_secondary_response(secondary_id, message) {
|
|
255
|
-
const connection = this.secondary_connections.get(secondary_id);
|
|
256
|
-
if (!connection) return;
|
|
257
|
-
|
|
258
|
-
try {
|
|
259
|
-
const parsed_message = typeof message === 'string' ? JSON.parse(message) : message;
|
|
260
|
-
|
|
261
|
-
switch (parsed_message.type) {
|
|
262
|
-
case 'auth_success':
|
|
263
|
-
connection.authenticated = true;
|
|
264
|
-
this.log.info('Secondary authentication successful', { secondary_id });
|
|
265
|
-
break;
|
|
266
|
-
|
|
267
|
-
case 'auth_failed':
|
|
268
|
-
this.log.error('Secondary authentication failed', { secondary_id });
|
|
269
|
-
connection.socket.end();
|
|
270
|
-
break;
|
|
271
|
-
|
|
272
|
-
case 'replication_ack':
|
|
273
|
-
this.handle_replication_acknowledgment(secondary_id, parsed_message);
|
|
274
|
-
break;
|
|
275
|
-
|
|
276
|
-
case 'ping_response':
|
|
277
|
-
connection.last_ping = Date.now();
|
|
278
|
-
break;
|
|
279
|
-
|
|
280
|
-
default:
|
|
281
|
-
this.log.debug('Unknown message from secondary', {
|
|
282
|
-
secondary_id,
|
|
283
|
-
type: parsed_message.type
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
} catch (error) {
|
|
287
|
-
this.log.error('Failed to handle secondary response', {
|
|
288
|
-
secondary_id,
|
|
289
|
-
error: error.message
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Processes replication acknowledgments and updates statistics.
|
|
296
|
-
* Tracks operation latency, success/failure rates, and removes pending operations.
|
|
297
|
-
* Updates average replication latency and logs replication results for monitoring.
|
|
298
|
-
* @param {string} secondary_id - Secondary node ID that sent acknowledgment
|
|
299
|
-
* @param {Object} ack_message - Acknowledgment message with status and sequence number
|
|
300
|
-
* @param {number} ack_message.sequence_number - Sequence number of acknowledged operation
|
|
301
|
-
* @param {string} ack_message.status - Status of replication ('success' or 'error')
|
|
302
|
-
* @param {string} [ack_message.error] - Error message if replication failed
|
|
303
|
-
* @returns {void}
|
|
304
|
-
*/
|
|
305
|
-
handle_replication_acknowledgment(secondary_id, ack_message) {
|
|
306
|
-
const connection = this.secondary_connections.get(secondary_id);
|
|
307
|
-
if (!connection) return;
|
|
308
|
-
|
|
309
|
-
const { sequence_number, status, error } = ack_message;
|
|
310
|
-
const pending_op = connection.pending_operations.get(sequence_number);
|
|
311
|
-
|
|
312
|
-
if (pending_op) {
|
|
313
|
-
const latency = Date.now() - pending_op.sent_at;
|
|
314
|
-
this.stats.total_replication_time_ms += latency;
|
|
315
|
-
|
|
316
|
-
if (status === 'success') {
|
|
317
|
-
this.stats.successful_replications++;
|
|
318
|
-
this.log.debug('Replication acknowledged', {
|
|
319
|
-
secondary_id,
|
|
320
|
-
sequence_number,
|
|
321
|
-
latency_ms: latency
|
|
322
|
-
});
|
|
323
|
-
} else {
|
|
324
|
-
this.stats.failed_replications++;
|
|
325
|
-
this.log.error('Replication failed on secondary', {
|
|
326
|
-
secondary_id,
|
|
327
|
-
sequence_number,
|
|
328
|
-
error,
|
|
329
|
-
latency_ms: latency
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
connection.pending_operations.delete(sequence_number);
|
|
334
|
-
|
|
335
|
-
// Update average latency
|
|
336
|
-
const total_ops = this.stats.successful_replications + this.stats.failed_replications;
|
|
337
|
-
this.stats.avg_replication_latency_ms = total_ops > 0
|
|
338
|
-
? Math.round(this.stats.total_replication_time_ms / total_ops)
|
|
339
|
-
: 0;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Queues a write operation for replication to secondary nodes.
|
|
345
|
-
* Creates replication item with sequence number and timestamps, adds to queue,
|
|
346
|
-
* and triggers processing if not already active. Skips if replication disabled.
|
|
347
|
-
* @param {string} operation - Type of operation (insert, update, delete, etc.)
|
|
348
|
-
* @param {string} collection - Name of the collection being modified
|
|
349
|
-
* @param {Object} data - Operation data including document and filter information
|
|
350
|
-
* @param {string|null} [transaction_id=null] - Optional transaction identifier
|
|
351
|
-
* @returns {void}
|
|
352
|
-
*/
|
|
353
|
-
queue_replication(operation, collection, data, transaction_id = null) {
|
|
354
|
-
if (!this.enabled || this.secondary_connections.size === 0) {
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const replication_item = {
|
|
359
|
-
operation,
|
|
360
|
-
collection,
|
|
361
|
-
data,
|
|
362
|
-
transaction_id,
|
|
363
|
-
timestamp: Date.now(),
|
|
364
|
-
sequence_number: ++this.sequence_number
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
this.replication_queue.push(replication_item);
|
|
368
|
-
this.stats.total_operations_replicated++;
|
|
369
|
-
|
|
370
|
-
this.log.debug('Queued operation for replication', {
|
|
371
|
-
operation,
|
|
372
|
-
collection,
|
|
373
|
-
sequence_number: replication_item.sequence_number,
|
|
374
|
-
queue_length: this.replication_queue.length
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
// Process queue if not already processing
|
|
378
|
-
if (!this.processing_replication) {
|
|
379
|
-
setImmediate(() => this.process_replication_queue());
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Initiates replication queue processing if not already active.
|
|
385
|
-
* Sets processing flag and begins queue processing loop to handle pending operations.
|
|
386
|
-
* @returns {void}
|
|
387
|
-
*/
|
|
388
|
-
start_replication_processing() {
|
|
389
|
-
if (this.processing_replication) return;
|
|
390
|
-
|
|
391
|
-
this.processing_replication = true;
|
|
392
|
-
this.process_replication_queue();
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Processes replication queue in batches with controlled timing.
|
|
397
|
-
* Continuously processes batches until queue is empty, with small delays
|
|
398
|
-
* between batches to prevent overwhelming secondary nodes.
|
|
399
|
-
* @returns {Promise<void>}
|
|
400
|
-
*/
|
|
401
|
-
async process_replication_queue() {
|
|
402
|
-
while (this.replication_queue.length > 0 && this.enabled) {
|
|
403
|
-
const batch = this.replication_queue.splice(0, this.batch_size);
|
|
404
|
-
|
|
405
|
-
if (batch.length === 0) break;
|
|
406
|
-
|
|
407
|
-
await this.replicate_batch(batch);
|
|
408
|
-
|
|
409
|
-
// Small delay between batches to prevent overwhelming secondaries
|
|
410
|
-
if (this.replication_queue.length > 0) {
|
|
411
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Continue processing if more items were added
|
|
416
|
-
if (this.replication_queue.length > 0) {
|
|
417
|
-
setImmediate(() => this.process_replication_queue());
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Sends a batch of operations to all authenticated secondary nodes.
|
|
423
|
-
* Creates replication message, encodes it, and sends to all connected secondaries.
|
|
424
|
-
* Tracks pending operations for acknowledgment and handles send failures gracefully.
|
|
425
|
-
* @param {Array<Object>} batch - Array of operations to replicate
|
|
426
|
-
* @returns {Promise<void>}
|
|
427
|
-
*/
|
|
428
|
-
async replicate_batch(batch) {
|
|
429
|
-
const replication_message = {
|
|
430
|
-
type: 'replication',
|
|
431
|
-
timestamp: Date.now(),
|
|
432
|
-
sequence_number: batch[0].sequence_number,
|
|
433
|
-
operations: batch
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
const encoded_message = encode_message(replication_message);
|
|
437
|
-
const authenticated_secondaries = Array.from(this.secondary_connections.values())
|
|
438
|
-
.filter(conn => conn.authenticated);
|
|
439
|
-
|
|
440
|
-
if (authenticated_secondaries.length === 0) {
|
|
441
|
-
this.log.warn('No authenticated secondaries available for replication');
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
this.log.debug('Replicating batch to secondaries', {
|
|
446
|
-
batch_size: batch.length,
|
|
447
|
-
secondary_count: authenticated_secondaries.length,
|
|
448
|
-
sequence_number: batch[0].sequence_number
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
// Send to all authenticated secondaries
|
|
452
|
-
for (const connection of authenticated_secondaries) {
|
|
453
|
-
try {
|
|
454
|
-
connection.socket.write(encoded_message);
|
|
455
|
-
|
|
456
|
-
// Track pending operations for acknowledgment
|
|
457
|
-
for (const operation of batch) {
|
|
458
|
-
connection.pending_operations.set(operation.sequence_number, {
|
|
459
|
-
operation,
|
|
460
|
-
sent_at: Date.now()
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
} catch (error) {
|
|
465
|
-
this.log.error('Failed to send replication to secondary', {
|
|
466
|
-
secondary_id: connection.id,
|
|
467
|
-
error: error.message
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Dynamically adds a new secondary node to the replication cluster.
|
|
475
|
-
* Validates that secondary doesn't already exist, establishes connection,
|
|
476
|
-
* and returns success confirmation with secondary ID.
|
|
477
|
-
* @param {Object} secondary - Secondary node configuration
|
|
478
|
-
* @param {string} secondary.id - Unique identifier for the secondary node
|
|
479
|
-
* @param {string} secondary.ip - IP address of the secondary node
|
|
480
|
-
* @param {number} secondary.port - Port number of the secondary node
|
|
481
|
-
* @param {string} secondary.private_key - Base64 encoded private key
|
|
482
|
-
* @returns {Promise<Object>} Operation result with success status and secondary ID
|
|
483
|
-
* @throws {Error} When secondary node with same ID already exists
|
|
484
|
-
*/
|
|
485
|
-
async add_secondary(secondary) {
|
|
486
|
-
const { id } = secondary;
|
|
487
|
-
|
|
488
|
-
if (this.secondary_connections.has(id)) {
|
|
489
|
-
throw new Error(`Secondary node ${id} already exists`);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
this.log.info('Adding new secondary node', { id });
|
|
493
|
-
|
|
494
|
-
await this.connect_to_secondary(secondary);
|
|
495
|
-
|
|
496
|
-
return {
|
|
497
|
-
success: true,
|
|
498
|
-
message: `Secondary node ${id} added successfully`,
|
|
499
|
-
secondary_id: id
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Removes a secondary node from the replication cluster.
|
|
505
|
-
* Closes connection, removes from tracking, and updates statistics.
|
|
506
|
-
* Provides clean disconnection and resource cleanup.
|
|
507
|
-
* @param {string} secondary_id - Unique identifier of secondary node to remove
|
|
508
|
-
* @returns {Object} Operation result with success status and secondary ID
|
|
509
|
-
* @throws {Error} When secondary node with specified ID is not found
|
|
510
|
-
*/
|
|
511
|
-
remove_secondary(secondary_id) {
|
|
512
|
-
const connection = this.secondary_connections.get(secondary_id);
|
|
513
|
-
|
|
514
|
-
if (!connection) {
|
|
515
|
-
throw new Error(`Secondary node ${secondary_id} not found`);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
this.log.info('Removing secondary node', { secondary_id });
|
|
519
|
-
|
|
520
|
-
connection.socket.end();
|
|
521
|
-
this.secondary_connections.delete(secondary_id);
|
|
522
|
-
this.stats.connected_secondaries = this.secondary_connections.size;
|
|
523
|
-
|
|
524
|
-
return {
|
|
525
|
-
success: true,
|
|
526
|
-
message: `Secondary node ${secondary_id} removed successfully`,
|
|
527
|
-
secondary_id
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* Forces synchronization with all secondary nodes and checks connectivity.
|
|
533
|
-
* Processes pending replication queue and sends ping messages to verify
|
|
534
|
-
* secondary node health and responsiveness.
|
|
535
|
-
* @returns {Promise<Object>} Synchronization result with status for each secondary
|
|
536
|
-
* @throws {Error} When replication is not enabled
|
|
537
|
-
*/
|
|
538
|
-
async sync_secondaries() {
|
|
539
|
-
if (!this.enabled) {
|
|
540
|
-
throw new Error('Replication is not enabled');
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
this.log.info('Forcing secondary synchronization');
|
|
544
|
-
|
|
545
|
-
// Process any pending replication queue
|
|
546
|
-
await this.process_replication_queue();
|
|
547
|
-
|
|
548
|
-
// Send ping to all secondaries to check connectivity
|
|
549
|
-
const ping_message = {
|
|
550
|
-
type: 'ping',
|
|
551
|
-
timestamp: Date.now()
|
|
552
|
-
};
|
|
553
|
-
|
|
554
|
-
const encoded_ping = encode_message(ping_message);
|
|
555
|
-
const results = [];
|
|
556
|
-
|
|
557
|
-
for (const [id, connection] of this.secondary_connections) {
|
|
558
|
-
try {
|
|
559
|
-
if (connection.authenticated) {
|
|
560
|
-
connection.socket.write(encoded_ping);
|
|
561
|
-
results.push({ secondary_id: id, status: 'ping_sent' });
|
|
562
|
-
} else {
|
|
563
|
-
results.push({ secondary_id: id, status: 'not_authenticated' });
|
|
564
|
-
}
|
|
565
|
-
} catch (error) {
|
|
566
|
-
results.push({
|
|
567
|
-
secondary_id: id,
|
|
568
|
-
status: 'error',
|
|
569
|
-
error: error.message
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
return {
|
|
575
|
-
success: true,
|
|
576
|
-
message: 'Secondary synchronization initiated',
|
|
577
|
-
results
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* Retrieves comprehensive replication status and statistics.
|
|
583
|
-
* Returns current configuration, connection states, queue status, and
|
|
584
|
-
* detailed statistics for monitoring and debugging purposes.
|
|
585
|
-
* @returns {Object} Complete replication status information
|
|
586
|
-
* @returns {boolean} returns.enabled - Whether replication is enabled
|
|
587
|
-
* @returns {string} returns.mode - Replication mode (async/sync)
|
|
588
|
-
* @returns {number} returns.connected_secondaries - Number of connected secondaries
|
|
589
|
-
* @returns {number} returns.queue_length - Current replication queue length
|
|
590
|
-
* @returns {boolean} returns.processing - Whether queue processing is active
|
|
591
|
-
* @returns {Object} returns.stats - Detailed replication statistics
|
|
592
|
-
* @returns {Array<Object>} returns.secondaries - Status of each secondary node
|
|
593
|
-
*/
|
|
594
|
-
get_replication_status() {
|
|
595
|
-
const secondary_status = [];
|
|
596
|
-
|
|
597
|
-
for (const [id, connection] of this.secondary_connections) {
|
|
598
|
-
secondary_status.push({
|
|
599
|
-
id,
|
|
600
|
-
ip: connection.ip,
|
|
601
|
-
port: connection.port,
|
|
602
|
-
authenticated: connection.authenticated,
|
|
603
|
-
last_ping: connection.last_ping,
|
|
604
|
-
pending_operations: connection.pending_operations.size,
|
|
605
|
-
connected: !connection.socket.destroyed
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
return {
|
|
610
|
-
enabled: this.enabled,
|
|
611
|
-
mode: this.mode,
|
|
612
|
-
connected_secondaries: this.stats.connected_secondaries,
|
|
613
|
-
queue_length: this.replication_queue.length,
|
|
614
|
-
processing: this.processing_replication,
|
|
615
|
-
stats: this.stats,
|
|
616
|
-
secondaries: secondary_status
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Evaluates health status of all secondary nodes.
|
|
622
|
-
* Checks authentication status, connection state, and ping responsiveness
|
|
623
|
-
* to determine overall health of each secondary node.
|
|
624
|
-
* @returns {Object} Health status summary and detailed node information
|
|
625
|
-
* @returns {number} returns.total_secondaries - Total number of secondary nodes
|
|
626
|
-
* @returns {number} returns.healthy_secondaries - Number of healthy secondary nodes
|
|
627
|
-
* @returns {Array<Object>} returns.secondaries - Detailed health status for each node
|
|
628
|
-
*/
|
|
629
|
-
get_secondary_health() {
|
|
630
|
-
const health_status = [];
|
|
631
|
-
const current_time = Date.now();
|
|
632
|
-
|
|
633
|
-
for (const [id, connection] of this.secondary_connections) {
|
|
634
|
-
const last_ping_age = current_time - connection.last_ping;
|
|
635
|
-
const is_healthy = connection.authenticated &&
|
|
636
|
-
!connection.socket.destroyed &&
|
|
637
|
-
last_ping_age < 30000; // 30 seconds
|
|
638
|
-
|
|
639
|
-
health_status.push({
|
|
640
|
-
id,
|
|
641
|
-
ip: connection.ip,
|
|
642
|
-
port: connection.port,
|
|
643
|
-
healthy: is_healthy,
|
|
644
|
-
authenticated: connection.authenticated,
|
|
645
|
-
connected: !connection.socket.destroyed,
|
|
646
|
-
last_ping_age_ms: last_ping_age,
|
|
647
|
-
pending_operations: connection.pending_operations.size
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
return {
|
|
652
|
-
total_secondaries: health_status.length,
|
|
653
|
-
healthy_secondaries: health_status.filter(s => s.healthy).length,
|
|
654
|
-
secondaries: health_status
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
/**
|
|
659
|
-
* Gracefully shuts down replication manager and closes all connections.
|
|
660
|
-
* Disables replication, closes secondary connections, clears queues,
|
|
661
|
-
* and resets all state for clean shutdown.
|
|
662
|
-
* @returns {Promise<void>}
|
|
663
|
-
*/
|
|
664
|
-
async shutdown() {
|
|
665
|
-
this.log.info('Shutting down replication manager');
|
|
666
|
-
|
|
667
|
-
this.enabled = false;
|
|
668
|
-
this.processing_replication = false;
|
|
669
|
-
|
|
670
|
-
// Close all secondary connections
|
|
671
|
-
for (const [id, connection] of this.secondary_connections) {
|
|
672
|
-
try {
|
|
673
|
-
connection.socket.end();
|
|
674
|
-
} catch (error) {
|
|
675
|
-
this.log.warn('Error closing secondary connection', {
|
|
676
|
-
secondary_id: id,
|
|
677
|
-
error: error.message
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
this.secondary_connections.clear();
|
|
683
|
-
this.replication_queue = [];
|
|
684
|
-
this.stats.connected_secondaries = 0;
|
|
685
|
-
|
|
686
|
-
this.log.info('Replication manager shutdown complete');
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/** @type {ReplicationManager|null} Singleton replication manager instance */
|
|
691
|
-
let replication_manager_instance = null;
|
|
692
|
-
|
|
693
|
-
/**
|
|
694
|
-
* Gets the replication manager singleton instance.
|
|
695
|
-
* Creates new instance if one doesn't exist, ensuring single manager per process.
|
|
696
|
-
* @returns {ReplicationManager} Replication manager singleton instance
|
|
697
|
-
*/
|
|
698
|
-
export const get_replication_manager = () => {
|
|
699
|
-
if (!replication_manager_instance) {
|
|
700
|
-
replication_manager_instance = new ReplicationManager();
|
|
701
|
-
}
|
|
702
|
-
return replication_manager_instance;
|
|
703
|
-
};
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* Initializes the replication manager singleton with configuration.
|
|
707
|
-
* Gets or creates manager instance and calls initialize to load settings
|
|
708
|
-
* and establish secondary connections.
|
|
709
|
-
* @returns {void}
|
|
710
|
-
*/
|
|
711
|
-
export const initialize_replication_manager = () => {
|
|
712
|
-
const manager = get_replication_manager();
|
|
713
|
-
manager.initialize();
|
|
714
|
-
};
|
|
715
|
-
|
|
716
|
-
/**
|
|
717
|
-
* Shuts down the replication manager singleton and cleans up resources.
|
|
718
|
-
* Calls shutdown on existing instance and resets singleton reference
|
|
719
|
-
* for clean process termination.
|
|
720
|
-
* @returns {Promise<void>}
|
|
721
|
-
*/
|
|
722
|
-
export const shutdown_replication_manager = async () => {
|
|
723
|
-
if (replication_manager_instance) {
|
|
724
|
-
await replication_manager_instance.shutdown();
|
|
725
|
-
replication_manager_instance = null;
|
|
726
|
-
}
|
|
727
|
-
};
|