@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.
- 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,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
|
-
};
|