@joystick.js/db-canary 0.0.0-canary.2275 → 0.0.0-canary.2277

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.
@@ -1,636 +0,0 @@
1
- /**
2
- * @fileoverview Write operation forwarder for JoystickDB secondary nodes.
3
- * Enables secondary nodes to forward write operations to the primary node in a
4
- * master-secondary architecture. Provides authentication, timeout handling, retry logic,
5
- * and comprehensive monitoring for reliable write operation forwarding with statistics.
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('write_forwarder');
15
-
16
- /**
17
- * Manages forwarding of write operations from secondary nodes to primary node.
18
- * Handles connection management, authentication, operation forwarding, timeout handling,
19
- * and provides comprehensive statistics for monitoring secondary node behavior.
20
- */
21
- class WriteForwarder {
22
- /**
23
- * Creates a new WriteForwarder instance with default configuration.
24
- * Initializes connection tracking, operation forwarding, and statistics collection.
25
- */
26
- constructor() {
27
- /** @type {Object|null} Connection to primary node */
28
- this.primary_connection = null;
29
-
30
- /** @type {boolean} Whether this node is operating in secondary mode */
31
- this.is_secondary_mode = false;
32
-
33
- /** @type {Object|null} Primary node configuration */
34
- this.primary_config = null;
35
-
36
- /** @type {Map<string, Object>} Map of pending forward operations by ID */
37
- this.pending_forwards = new Map();
38
-
39
- /** @type {number} Timeout for forward operations in milliseconds */
40
- this.forward_timeout_ms = 10000;
41
-
42
- /** @type {number} Delay between reconnection attempts in milliseconds */
43
- this.reconnect_delay_ms = 5000;
44
-
45
- /** @type {Object} Logger instance for write forwarding events */
46
- this.log = create_context_logger();
47
-
48
- /** @type {Object} Write forwarding statistics and metrics */
49
- this.stats = {
50
- total_forwards: 0,
51
- successful_forwards: 0,
52
- failed_forwards: 0,
53
- avg_forward_latency_ms: 0,
54
- total_forward_time_ms: 0,
55
- connection_attempts: 0,
56
- last_connection_attempt: null
57
- };
58
- }
59
-
60
- /**
61
- * Initializes write forwarder with configuration from settings.
62
- * Checks if node is configured as secondary, loads primary configuration,
63
- * and establishes connection to primary node for write forwarding.
64
- * @returns {void}
65
- */
66
- initialize() {
67
- try {
68
- const settings = get_settings();
69
-
70
- // Check if this node is configured as secondary
71
- if (settings.mode !== 'secondary' || !settings.primary) {
72
- this.log.info('Node not configured as secondary - write forwarding disabled');
73
- return;
74
- }
75
-
76
- this.is_secondary_mode = true;
77
- this.primary_config = settings.primary;
78
- this.forward_timeout_ms = settings.replication?.timeout_ms || 10000;
79
-
80
- this.log.info('Initializing write forwarder for secondary mode', {
81
- primary_ip: this.primary_config.ip,
82
- primary_port: this.primary_config.port,
83
- timeout_ms: this.forward_timeout_ms
84
- });
85
-
86
- // Connect to primary node
87
- this.connect_to_primary();
88
-
89
- } catch (error) {
90
- this.log.warn('Could not initialize write forwarder - settings not loaded', {
91
- error: error.message
92
- });
93
- }
94
- }
95
-
96
- /**
97
- * Establishes connection to the primary node with authentication and error handling.
98
- * Sets up TCP connection, message parsing, event handlers, and automatic reconnection.
99
- * Tracks connection attempts and handles authentication with the primary node.
100
- * @returns {Promise<void>}
101
- */
102
- async connect_to_primary() {
103
- if (!this.is_secondary_mode || !this.primary_config) {
104
- return;
105
- }
106
-
107
- const { ip, port, public_key } = this.primary_config;
108
-
109
- this.log.info('Connecting to primary node', { ip, port });
110
- this.stats.connection_attempts++;
111
- this.stats.last_connection_attempt = Date.now();
112
-
113
- try {
114
- const socket = new net.Socket();
115
- const message_parser = create_message_parser();
116
-
117
- socket.connect(port, ip, () => {
118
- this.log.info('Connected to primary node', { ip, port });
119
-
120
- // Send authentication message
121
- this.authenticate_with_primary(socket, public_key);
122
- });
123
-
124
- socket.on('data', (data) => {
125
- try {
126
- const messages = message_parser.parse_messages(data);
127
- for (const message of messages) {
128
- this.handle_primary_response(message);
129
- }
130
- } catch (error) {
131
- this.log.error('Failed to parse primary response', {
132
- error: error.message
133
- });
134
- }
135
- });
136
-
137
- socket.on('error', (error) => {
138
- this.log.error('Primary connection error', {
139
- error: error.message
140
- });
141
-
142
- this.primary_connection = null;
143
-
144
- // Fail all pending forwards
145
- this.fail_pending_forwards('Primary connection error');
146
-
147
- // Attempt reconnection after delay
148
- setTimeout(() => {
149
- this.connect_to_primary();
150
- }, this.reconnect_delay_ms);
151
- });
152
-
153
- socket.on('close', () => {
154
- this.log.warn('Primary connection closed');
155
- this.primary_connection = null;
156
-
157
- // Fail all pending forwards
158
- this.fail_pending_forwards('Primary connection closed');
159
-
160
- // Attempt reconnection after delay
161
- setTimeout(() => {
162
- this.connect_to_primary();
163
- }, this.reconnect_delay_ms);
164
- });
165
-
166
- this.primary_connection = {
167
- socket,
168
- authenticated: false,
169
- last_ping: Date.now()
170
- };
171
-
172
- } catch (error) {
173
- this.log.error('Failed to connect to primary', {
174
- ip,
175
- port,
176
- error: error.message
177
- });
178
-
179
- // Retry connection after delay
180
- setTimeout(() => {
181
- this.connect_to_primary();
182
- }, this.reconnect_delay_ms);
183
- }
184
- }
185
-
186
- /**
187
- * Authenticates with the primary node using HMAC-SHA256 signature.
188
- * Creates timestamped signature using public key and sends authentication message.
189
- * Identifies this node as a secondary for proper handling by the primary.
190
- * @param {net.Socket} socket - Socket connection to primary node
191
- * @param {string} public_key - Base64 encoded public key for signing
192
- * @returns {void}
193
- */
194
- authenticate_with_primary(socket, public_key) {
195
- try {
196
- const timestamp = Date.now();
197
- const node_id = `secondary-${process.pid}`;
198
- const message_to_sign = `${node_id}:${timestamp}`;
199
-
200
- // Create signature using public key (in real implementation, this would use private key)
201
- const signature = crypto
202
- .createHmac('sha256', Buffer.from(public_key, 'base64'))
203
- .update(message_to_sign)
204
- .digest('base64');
205
-
206
- const auth_message = {
207
- op: 'authentication',
208
- data: {
209
- password: signature, // Use signature as password for now
210
- node_type: 'secondary',
211
- node_id
212
- }
213
- };
214
-
215
- const encoded_message = encode_message(auth_message);
216
- socket.write(encoded_message);
217
-
218
- this.log.debug('Sent authentication to primary');
219
-
220
- } catch (error) {
221
- this.log.error('Failed to authenticate with primary', {
222
- error: error.message
223
- });
224
- }
225
- }
226
-
227
- /**
228
- * Processes responses from the primary node including authentication and operation results.
229
- * Routes different message types to appropriate handlers and maintains connection state.
230
- * Handles authentication confirmations, forwarded operation responses, and ping responses.
231
- * @param {Object|string} message - Parsed message from primary node
232
- * @returns {void}
233
- */
234
- handle_primary_response(message) {
235
- if (!this.primary_connection) return;
236
-
237
- try {
238
- const parsed_message = typeof message === 'string' ? JSON.parse(message) : message;
239
-
240
- // Handle authentication response
241
- if (parsed_message.ok === 1 && parsed_message.message === 'Authentication successful') {
242
- this.primary_connection.authenticated = true;
243
- this.log.info('Primary authentication successful');
244
- return;
245
- }
246
-
247
- // Handle forwarded operation responses
248
- if (parsed_message.forward_id) {
249
- this.handle_forward_response(parsed_message);
250
- return;
251
- }
252
-
253
- // Handle ping responses
254
- if (parsed_message.ok === 1 && !parsed_message.forward_id) {
255
- this.primary_connection.last_ping = Date.now();
256
- return;
257
- }
258
-
259
- this.log.debug('Unhandled primary response', {
260
- type: typeof parsed_message,
261
- keys: Object.keys(parsed_message)
262
- });
263
-
264
- } catch (error) {
265
- this.log.error('Failed to handle primary response', {
266
- error: error.message
267
- });
268
- }
269
- }
270
-
271
- /**
272
- * Processes responses to forwarded operations and updates statistics.
273
- * Tracks operation latency, success/failure rates, and forwards responses to original clients.
274
- * Clears timeouts and removes pending operations from tracking.
275
- * @param {Object} response - Response from primary node with forward_id
276
- * @param {string} response.forward_id - Unique identifier for the forwarded operation
277
- * @param {number|boolean} response.ok - Success indicator (1/true for success, 0/false for failure)
278
- * @param {string} [response.error] - Error message if operation failed
279
- * @returns {void}
280
- */
281
- handle_forward_response(response) {
282
- const { forward_id } = response;
283
- const pending_forward = this.pending_forwards.get(forward_id);
284
-
285
- if (!pending_forward) {
286
- this.log.warn('Received response for unknown forward', { forward_id });
287
- return;
288
- }
289
-
290
- const latency = Date.now() - pending_forward.sent_at;
291
- this.stats.total_forward_time_ms += latency;
292
-
293
- // Clear timeout
294
- clearTimeout(pending_forward.timeout);
295
-
296
- // Remove from pending forwards
297
- this.pending_forwards.delete(forward_id);
298
-
299
- // Create response without forward_id for client
300
- const client_response = { ...response };
301
- delete client_response.forward_id;
302
-
303
- if (response.ok === 1 || response.ok === true) {
304
- this.stats.successful_forwards++;
305
- this.log.debug('Forward operation successful', {
306
- forward_id,
307
- latency_ms: latency,
308
- operation: pending_forward.operation
309
- });
310
- } else {
311
- this.stats.failed_forwards++;
312
- this.log.error('Forward operation failed', {
313
- forward_id,
314
- latency_ms: latency,
315
- operation: pending_forward.operation,
316
- error: response.error
317
- });
318
- }
319
-
320
- // Update average latency
321
- const total_ops = this.stats.successful_forwards + this.stats.failed_forwards;
322
- this.stats.avg_forward_latency_ms = total_ops > 0
323
- ? Math.round(this.stats.total_forward_time_ms / total_ops)
324
- : 0;
325
-
326
- // Send response to original client
327
- try {
328
- const encoded_response = encode_message(client_response);
329
- pending_forward.client_socket.write(encoded_response);
330
- } catch (error) {
331
- this.log.error('Failed to send response to client', {
332
- forward_id,
333
- error: error.message
334
- });
335
- }
336
- }
337
-
338
- /**
339
- * Determines if an operation should be forwarded to the primary node.
340
- * Checks if node is in secondary mode and if operation is a write operation
341
- * that requires forwarding to maintain data consistency.
342
- * @param {string} op_type - Type of operation to check
343
- * @returns {boolean} True if operation should be forwarded to primary
344
- */
345
- should_forward_operation(op_type) {
346
- if (!this.is_secondary_mode) {
347
- return false;
348
- }
349
-
350
- const write_operations = [
351
- 'insert_one',
352
- 'update_one',
353
- 'delete_one',
354
- 'bulk_write',
355
- 'create_index',
356
- 'drop_index'
357
- ];
358
-
359
- return write_operations.includes(op_type);
360
- }
361
-
362
- /**
363
- * Forwards a write operation to the primary node with timeout and error handling.
364
- * Creates forward request with unique ID, sends to primary, and tracks pending operation.
365
- * Handles connection failures and provides error responses to clients when needed.
366
- * @param {net.Socket} client_socket - Original client socket for response delivery
367
- * @param {string} op_type - Type of operation to forward (insert_one, update_one, etc.)
368
- * @param {Object} data - Operation data including collection, document, and filters
369
- * @returns {Promise<boolean>} True if forwarding was initiated or handled with error
370
- */
371
- async forward_operation(client_socket, op_type, data) {
372
- if (!this.is_secondary_mode || !this.should_forward_operation(op_type)) {
373
- return false;
374
- }
375
-
376
- if (!this.primary_connection || !this.primary_connection.authenticated) {
377
- this.log.error('Cannot forward operation - not connected to primary', {
378
- operation: op_type,
379
- connected: !!this.primary_connection,
380
- authenticated: this.primary_connection?.authenticated || false
381
- });
382
-
383
- // Send error response to client
384
- const error_response = {
385
- ok: 0,
386
- error: 'Secondary node not connected to primary'
387
- };
388
- const encoded_error = encode_message(error_response);
389
- client_socket.write(encoded_error);
390
-
391
- return true; // Operation was handled (with error)
392
- }
393
-
394
- const forward_id = this.generate_forward_id();
395
-
396
- this.log.debug('Forwarding operation to primary', {
397
- forward_id,
398
- operation: op_type,
399
- client_id: client_socket.id
400
- });
401
-
402
- // Create forward request with metadata
403
- const forward_request = {
404
- op: op_type,
405
- data,
406
- forward_id,
407
- forwarded_by: `secondary-${process.pid}`,
408
- original_client_id: client_socket.id,
409
- timestamp: Date.now()
410
- };
411
-
412
- try {
413
- const encoded_request = encode_message(forward_request);
414
- this.primary_connection.socket.write(encoded_request);
415
-
416
- // Track pending forward with timeout
417
- const timeout = setTimeout(() => {
418
- this.handle_forward_timeout(forward_id);
419
- }, this.forward_timeout_ms);
420
-
421
- this.pending_forwards.set(forward_id, {
422
- client_socket,
423
- operation: op_type,
424
- data,
425
- sent_at: Date.now(),
426
- timeout
427
- });
428
-
429
- this.stats.total_forwards++;
430
-
431
- return true; // Operation forwarded successfully
432
-
433
- } catch (error) {
434
- this.log.error('Failed to forward operation', {
435
- forward_id,
436
- operation: op_type,
437
- error: error.message
438
- });
439
-
440
- // Send error response to client
441
- const error_response = {
442
- ok: 0,
443
- error: `Failed to forward operation: ${error.message}`
444
- };
445
- const encoded_error = encode_message(error_response);
446
- client_socket.write(encoded_error);
447
-
448
- return true; // Operation was handled (with error)
449
- }
450
- }
451
-
452
- /**
453
- * Handles timeout for forwarded operations that don't receive responses.
454
- * Removes pending operation, updates statistics, and sends timeout error to client.
455
- * Provides graceful handling of network delays or primary node issues.
456
- * @param {string} forward_id - Unique identifier of the timed-out forward operation
457
- * @returns {void}
458
- */
459
- handle_forward_timeout(forward_id) {
460
- const pending_forward = this.pending_forwards.get(forward_id);
461
-
462
- if (!pending_forward) {
463
- return;
464
- }
465
-
466
- this.log.error('Forward operation timed out', {
467
- forward_id,
468
- operation: pending_forward.operation,
469
- timeout_ms: this.forward_timeout_ms
470
- });
471
-
472
- this.pending_forwards.delete(forward_id);
473
- this.stats.failed_forwards++;
474
-
475
- // Update average latency
476
- const latency = Date.now() - pending_forward.sent_at;
477
- this.stats.total_forward_time_ms += latency;
478
- const total_ops = this.stats.successful_forwards + this.stats.failed_forwards;
479
- this.stats.avg_forward_latency_ms = total_ops > 0
480
- ? Math.round(this.stats.total_forward_time_ms / total_ops)
481
- : 0;
482
-
483
- // Send timeout error to client
484
- try {
485
- const error_response = {
486
- ok: 0,
487
- error: 'Operation forwarding timed out'
488
- };
489
- const encoded_error = encode_message(error_response);
490
- pending_forward.client_socket.write(encoded_error);
491
- } catch (error) {
492
- this.log.error('Failed to send timeout error to client', {
493
- forward_id,
494
- error: error.message
495
- });
496
- }
497
- }
498
-
499
- /**
500
- * Fails all pending forward operations with a specified reason.
501
- * Clears timeouts, sends error responses to clients, and updates statistics.
502
- * Used when primary connection is lost or during shutdown procedures.
503
- * @param {string} reason - Reason for failing all pending operations
504
- * @returns {void}
505
- */
506
- fail_pending_forwards(reason) {
507
- for (const [forward_id, pending_forward] of this.pending_forwards) {
508
- clearTimeout(pending_forward.timeout);
509
-
510
- this.log.error('Failing pending forward operation', {
511
- forward_id,
512
- operation: pending_forward.operation,
513
- reason
514
- });
515
-
516
- // Send error response to client
517
- try {
518
- const error_response = {
519
- ok: 0,
520
- error: `Forward operation failed: ${reason}`
521
- };
522
- const encoded_error = encode_message(error_response);
523
- pending_forward.client_socket.write(encoded_error);
524
- } catch (error) {
525
- this.log.error('Failed to send error to client', {
526
- forward_id,
527
- error: error.message
528
- });
529
- }
530
- }
531
-
532
- this.stats.failed_forwards += this.pending_forwards.size;
533
- this.pending_forwards.clear();
534
- }
535
-
536
- /**
537
- * Generates a unique identifier for forward operations.
538
- * Creates timestamp-based ID with random suffix for uniqueness and traceability.
539
- * @returns {string} Unique forward operation identifier
540
- */
541
- generate_forward_id() {
542
- return `fwd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
543
- }
544
-
545
- /**
546
- * Retrieves comprehensive write forwarder status and statistics.
547
- * Returns current configuration, connection state, pending operations,
548
- * and detailed statistics for monitoring and debugging purposes.
549
- * @returns {Object} Complete write forwarder status information
550
- * @returns {boolean} returns.enabled - Whether write forwarding is enabled
551
- * @returns {boolean} returns.connected_to_primary - Whether connected and authenticated to primary
552
- * @returns {Object|null} returns.primary_config - Primary node configuration (IP and port)
553
- * @returns {number} returns.pending_forwards - Number of pending forward operations
554
- * @returns {Object} returns.stats - Detailed forwarding statistics and metrics
555
- */
556
- get_forwarder_status() {
557
- return {
558
- enabled: this.is_secondary_mode,
559
- connected_to_primary: !!this.primary_connection && this.primary_connection.authenticated,
560
- primary_config: this.primary_config ? {
561
- ip: this.primary_config.ip,
562
- port: this.primary_config.port
563
- } : null,
564
- pending_forwards: this.pending_forwards.size,
565
- stats: this.stats
566
- };
567
- }
568
-
569
- /**
570
- * Gracefully shuts down write forwarder and closes primary connection.
571
- * Fails all pending operations, closes connection to primary, and resets state
572
- * for clean shutdown without leaving clients waiting for responses.
573
- * @returns {Promise<void>}
574
- */
575
- async shutdown() {
576
- this.log.info('Shutting down write forwarder');
577
-
578
- // Fail all pending forwards
579
- this.fail_pending_forwards('Server shutting down');
580
-
581
- // Close primary connection
582
- if (this.primary_connection) {
583
- try {
584
- this.primary_connection.socket.end();
585
- } catch (error) {
586
- this.log.warn('Error closing primary connection', {
587
- error: error.message
588
- });
589
- }
590
- this.primary_connection = null;
591
- }
592
-
593
- this.is_secondary_mode = false;
594
-
595
- this.log.info('Write forwarder shutdown complete');
596
- }
597
- }
598
-
599
- /** @type {WriteForwarder|null} Singleton write forwarder instance */
600
- let write_forwarder_instance = null;
601
-
602
- /**
603
- * Gets the write forwarder singleton instance.
604
- * Creates new instance if one doesn't exist, ensuring single forwarder per process.
605
- * @returns {WriteForwarder} Write forwarder singleton instance
606
- */
607
- export const get_write_forwarder = () => {
608
- if (!write_forwarder_instance) {
609
- write_forwarder_instance = new WriteForwarder();
610
- }
611
- return write_forwarder_instance;
612
- };
613
-
614
- /**
615
- * Initializes the write forwarder singleton with configuration.
616
- * Gets or creates forwarder instance and calls initialize to load settings
617
- * and establish primary connection if in secondary mode.
618
- * @returns {void}
619
- */
620
- export const initialize_write_forwarder = () => {
621
- const forwarder = get_write_forwarder();
622
- forwarder.initialize();
623
- };
624
-
625
- /**
626
- * Shuts down the write forwarder singleton and cleans up resources.
627
- * Calls shutdown on existing instance and resets singleton reference
628
- * for clean process termination.
629
- * @returns {Promise<void>}
630
- */
631
- export const shutdown_write_forwarder = async () => {
632
- if (write_forwarder_instance) {
633
- await write_forwarder_instance.shutdown();
634
- write_forwarder_instance = null;
635
- }
636
- };