@joystick.js/db-canary 0.0.0-canary.2274 → 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.
Files changed (39) hide show
  1. package/README.md +87 -104
  2. package/debug_test_runner.js +208 -0
  3. package/dist/client/index.js +1 -1
  4. package/dist/server/cluster/master.js +2 -2
  5. package/dist/server/cluster/worker.js +1 -1
  6. package/dist/server/index.js +1 -1
  7. package/dist/server/lib/auto_index_manager.js +1 -1
  8. package/dist/server/lib/bulk_insert_optimizer.js +1 -1
  9. package/dist/server/lib/http_server.js +3 -3
  10. package/dist/server/lib/operation_dispatcher.js +1 -1
  11. package/dist/server/lib/operations/admin.js +1 -1
  12. package/dist/server/lib/operations/update_one.js +1 -1
  13. package/dist/server/lib/simple_sync_manager.js +1 -0
  14. package/dist/server/lib/sync_receiver.js +1 -0
  15. package/full_debug_test_runner.js +197 -0
  16. package/package.json +10 -7
  17. package/src/client/index.js +1 -0
  18. package/src/server/cluster/master.js +8 -2
  19. package/src/server/cluster/worker.js +9 -3
  20. package/src/server/index.js +25 -24
  21. package/src/server/lib/auto_index_manager.js +8 -3
  22. package/src/server/lib/bulk_insert_optimizer.js +79 -0
  23. package/src/server/lib/http_server.js +7 -0
  24. package/src/server/lib/operation_dispatcher.js +16 -10
  25. package/src/server/lib/operations/admin.js +64 -31
  26. package/src/server/lib/operations/update_one.js +251 -1
  27. package/src/server/lib/simple_sync_manager.js +444 -0
  28. package/src/server/lib/sync_receiver.js +461 -0
  29. package/tests/client/index.test.js +7 -0
  30. package/tests/performance/isolated_5000000_test.js +184 -0
  31. package/tests/server/lib/http_server.test.js +3 -12
  32. package/tests/server/lib/operations/update_one.test.js +161 -0
  33. package/tests/server/lib/simple_sync_system.test.js +124 -0
  34. package/dist/server/lib/replication_manager.js +0 -1
  35. package/dist/server/lib/write_forwarder.js +0 -1
  36. package/src/server/lib/replication_manager.js +0 -727
  37. package/src/server/lib/write_forwarder.js +0 -636
  38. package/tests/server/lib/replication_manager.test.js +0 -202
  39. 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
- };