@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.
- package/README.md +87 -104
- package/debug_test_runner.js +208 -0
- package/dist/client/index.js +1 -1
- package/dist/server/cluster/master.js +2 -2
- package/dist/server/cluster/worker.js +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/lib/auto_index_manager.js +1 -1
- package/dist/server/lib/bulk_insert_optimizer.js +1 -1
- package/dist/server/lib/http_server.js +3 -3
- package/dist/server/lib/operation_dispatcher.js +1 -1
- package/dist/server/lib/operations/admin.js +1 -1
- package/dist/server/lib/operations/update_one.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 +10 -7
- package/src/client/index.js +1 -0
- package/src/server/cluster/master.js +8 -2
- package/src/server/cluster/worker.js +9 -3
- package/src/server/index.js +25 -24
- package/src/server/lib/auto_index_manager.js +8 -3
- package/src/server/lib/bulk_insert_optimizer.js +79 -0
- package/src/server/lib/http_server.js +7 -0
- package/src/server/lib/operation_dispatcher.js +16 -10
- package/src/server/lib/operations/admin.js +64 -31
- package/src/server/lib/operations/update_one.js +251 -1
- package/src/server/lib/simple_sync_manager.js +444 -0
- package/src/server/lib/sync_receiver.js +461 -0
- package/tests/client/index.test.js +7 -0
- package/tests/performance/isolated_5000000_test.js +184 -0
- package/tests/server/lib/http_server.test.js +3 -12
- package/tests/server/lib/operations/update_one.test.js +161 -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
|
@@ -175,10 +175,228 @@ const apply_pull_operator = (document, operations) => {
|
|
|
175
175
|
return updated_document;
|
|
176
176
|
};
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Applies $add_to_set operator to document.
|
|
180
|
+
* @param {Object} document - Document to update
|
|
181
|
+
* @param {Object} operations - AddToSet operations
|
|
182
|
+
* @returns {Object} Updated document
|
|
183
|
+
*/
|
|
184
|
+
const apply_add_to_set_operator = (document, operations) => {
|
|
185
|
+
let updated_document = { ...document };
|
|
186
|
+
|
|
187
|
+
for (const [field, value] of Object.entries(operations)) {
|
|
188
|
+
if (field.includes('.')) {
|
|
189
|
+
// Handle nested field add_to_set with dot notation
|
|
190
|
+
let current_array = get_nested_field(updated_document, field);
|
|
191
|
+
if (!Array.isArray(current_array)) {
|
|
192
|
+
current_array = [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Only add if the value doesn't already exist in the array
|
|
196
|
+
if (!current_array.includes(value)) {
|
|
197
|
+
current_array = [...current_array, value];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
updated_document = set_nested_field(updated_document, field, current_array);
|
|
201
|
+
} else {
|
|
202
|
+
// Handle simple field add_to_set
|
|
203
|
+
if (!Array.isArray(updated_document[field])) {
|
|
204
|
+
updated_document[field] = [];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Only add if the value doesn't already exist in the array
|
|
208
|
+
if (!updated_document[field].includes(value)) {
|
|
209
|
+
updated_document[field] = [...updated_document[field], value];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return updated_document;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Applies $pull_all operator to document.
|
|
219
|
+
* @param {Object} document - Document to update
|
|
220
|
+
* @param {Object} operations - PullAll operations
|
|
221
|
+
* @returns {Object} Updated document
|
|
222
|
+
*/
|
|
223
|
+
const apply_pull_all_operator = (document, operations) => {
|
|
224
|
+
const updated_document = { ...document };
|
|
225
|
+
for (const [field, values] of Object.entries(operations)) {
|
|
226
|
+
if (Array.isArray(updated_document[field]) && Array.isArray(values)) {
|
|
227
|
+
updated_document[field] = updated_document[field].filter(item => !values.includes(item));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return updated_document;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Applies $pop operator to document.
|
|
235
|
+
* @param {Object} document - Document to update
|
|
236
|
+
* @param {Object} operations - Pop operations
|
|
237
|
+
* @returns {Object} Updated document
|
|
238
|
+
*/
|
|
239
|
+
const apply_pop_operator = (document, operations) => {
|
|
240
|
+
const updated_document = { ...document };
|
|
241
|
+
for (const [field, direction] of Object.entries(operations)) {
|
|
242
|
+
if (Array.isArray(updated_document[field]) && updated_document[field].length > 0) {
|
|
243
|
+
if (direction === 1 || direction === '1') {
|
|
244
|
+
// Remove last element
|
|
245
|
+
updated_document[field] = updated_document[field].slice(0, -1);
|
|
246
|
+
} else if (direction === -1 || direction === '-1') {
|
|
247
|
+
// Remove first element
|
|
248
|
+
updated_document[field] = updated_document[field].slice(1);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return updated_document;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Applies $rename operator to document.
|
|
257
|
+
* @param {Object} document - Document to update
|
|
258
|
+
* @param {Object} operations - Rename operations
|
|
259
|
+
* @returns {Object} Updated document
|
|
260
|
+
*/
|
|
261
|
+
const apply_rename_operator = (document, operations) => {
|
|
262
|
+
let updated_document = { ...document };
|
|
263
|
+
|
|
264
|
+
for (const [old_field, new_field] of Object.entries(operations)) {
|
|
265
|
+
if (old_field.includes('.') || new_field.includes('.')) {
|
|
266
|
+
// Handle nested field renames with dot notation
|
|
267
|
+
const old_value = get_nested_field(updated_document, old_field);
|
|
268
|
+
if (old_value !== undefined) {
|
|
269
|
+
updated_document = set_nested_field(updated_document, new_field, old_value);
|
|
270
|
+
updated_document = unset_nested_field(updated_document, old_field);
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
// Handle simple field renames
|
|
274
|
+
if (old_field in updated_document) {
|
|
275
|
+
updated_document[new_field] = updated_document[old_field];
|
|
276
|
+
delete updated_document[old_field];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return updated_document;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Applies $min operator to document.
|
|
286
|
+
* @param {Object} document - Document to update
|
|
287
|
+
* @param {Object} operations - Min operations
|
|
288
|
+
* @returns {Object} Updated document
|
|
289
|
+
*/
|
|
290
|
+
const apply_min_operator = (document, operations) => {
|
|
291
|
+
let updated_document = { ...document };
|
|
292
|
+
|
|
293
|
+
for (const [field, value] of Object.entries(operations)) {
|
|
294
|
+
if (field.includes('.')) {
|
|
295
|
+
// Handle nested field min with dot notation
|
|
296
|
+
const current_value = get_nested_field(updated_document, field);
|
|
297
|
+
if (current_value === undefined || value < current_value) {
|
|
298
|
+
updated_document = set_nested_field(updated_document, field, value);
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
// Handle simple field min
|
|
302
|
+
if (!(field in updated_document) || value < updated_document[field]) {
|
|
303
|
+
updated_document[field] = value;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return updated_document;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Applies $max operator to document.
|
|
313
|
+
* @param {Object} document - Document to update
|
|
314
|
+
* @param {Object} operations - Max operations
|
|
315
|
+
* @returns {Object} Updated document
|
|
316
|
+
*/
|
|
317
|
+
const apply_max_operator = (document, operations) => {
|
|
318
|
+
let updated_document = { ...document };
|
|
319
|
+
|
|
320
|
+
for (const [field, value] of Object.entries(operations)) {
|
|
321
|
+
if (field.includes('.')) {
|
|
322
|
+
// Handle nested field max with dot notation
|
|
323
|
+
const current_value = get_nested_field(updated_document, field);
|
|
324
|
+
if (current_value === undefined || value > current_value) {
|
|
325
|
+
updated_document = set_nested_field(updated_document, field, value);
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
// Handle simple field max
|
|
329
|
+
if (!(field in updated_document) || value > updated_document[field]) {
|
|
330
|
+
updated_document[field] = value;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return updated_document;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Applies $mul operator to document.
|
|
340
|
+
* @param {Object} document - Document to update
|
|
341
|
+
* @param {Object} operations - Multiply operations
|
|
342
|
+
* @returns {Object} Updated document
|
|
343
|
+
*/
|
|
344
|
+
const apply_mul_operator = (document, operations) => {
|
|
345
|
+
let updated_document = { ...document };
|
|
346
|
+
|
|
347
|
+
for (const [field, value] of Object.entries(operations)) {
|
|
348
|
+
if (field.includes('.')) {
|
|
349
|
+
// Handle nested field multiply with dot notation
|
|
350
|
+
const current_value = get_nested_field(updated_document, field) || 0;
|
|
351
|
+
updated_document = set_nested_field(updated_document, field, current_value * value);
|
|
352
|
+
} else {
|
|
353
|
+
// Handle simple field multiply
|
|
354
|
+
updated_document[field] = (updated_document[field] || 0) * value;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return updated_document;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Applies $current_date operator to document.
|
|
363
|
+
* @param {Object} document - Document to update
|
|
364
|
+
* @param {Object} operations - CurrentDate operations
|
|
365
|
+
* @returns {Object} Updated document
|
|
366
|
+
*/
|
|
367
|
+
const apply_current_date_operator = (document, operations) => {
|
|
368
|
+
let updated_document = { ...document };
|
|
369
|
+
const current_date = new Date();
|
|
370
|
+
|
|
371
|
+
for (const [field, type_spec] of Object.entries(operations)) {
|
|
372
|
+
let date_value;
|
|
373
|
+
|
|
374
|
+
if (type_spec === true) {
|
|
375
|
+
date_value = current_date;
|
|
376
|
+
} else if (typeof type_spec === 'object' && type_spec !== null && type_spec.$type === 'date') {
|
|
377
|
+
date_value = current_date;
|
|
378
|
+
} else if (typeof type_spec === 'object' && type_spec !== null && type_spec.$type === 'timestamp') {
|
|
379
|
+
date_value = current_date.toISOString();
|
|
380
|
+
} else {
|
|
381
|
+
date_value = current_date;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (field.includes('.')) {
|
|
385
|
+
// Handle nested field current date with dot notation
|
|
386
|
+
updated_document = set_nested_field(updated_document, field, date_value);
|
|
387
|
+
} else {
|
|
388
|
+
// Handle simple field current date
|
|
389
|
+
updated_document[field] = date_value;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return updated_document;
|
|
394
|
+
};
|
|
395
|
+
|
|
178
396
|
/**
|
|
179
397
|
* Applies MongoDB-style update operators to a document.
|
|
180
398
|
* @param {Object} document - Original document
|
|
181
|
-
* @param {Object} update_operations - Update operations
|
|
399
|
+
* @param {Object} update_operations - Update operations with supported operators
|
|
182
400
|
* @returns {Object} Updated document
|
|
183
401
|
* @throws {Error} When unsupported update operator is used
|
|
184
402
|
*/
|
|
@@ -207,6 +425,38 @@ const apply_update_operators = (document, update_operations) => {
|
|
|
207
425
|
updated_document = apply_pull_operator(updated_document, operations);
|
|
208
426
|
break;
|
|
209
427
|
|
|
428
|
+
case '$add_to_set':
|
|
429
|
+
updated_document = apply_add_to_set_operator(updated_document, operations);
|
|
430
|
+
break;
|
|
431
|
+
|
|
432
|
+
case '$pull_all':
|
|
433
|
+
updated_document = apply_pull_all_operator(updated_document, operations);
|
|
434
|
+
break;
|
|
435
|
+
|
|
436
|
+
case '$pop':
|
|
437
|
+
updated_document = apply_pop_operator(updated_document, operations);
|
|
438
|
+
break;
|
|
439
|
+
|
|
440
|
+
case '$rename':
|
|
441
|
+
updated_document = apply_rename_operator(updated_document, operations);
|
|
442
|
+
break;
|
|
443
|
+
|
|
444
|
+
case '$min':
|
|
445
|
+
updated_document = apply_min_operator(updated_document, operations);
|
|
446
|
+
break;
|
|
447
|
+
|
|
448
|
+
case '$max':
|
|
449
|
+
updated_document = apply_max_operator(updated_document, operations);
|
|
450
|
+
break;
|
|
451
|
+
|
|
452
|
+
case '$mul':
|
|
453
|
+
updated_document = apply_mul_operator(updated_document, operations);
|
|
454
|
+
break;
|
|
455
|
+
|
|
456
|
+
case '$current_date':
|
|
457
|
+
updated_document = apply_current_date_operator(updated_document, operations);
|
|
458
|
+
break;
|
|
459
|
+
|
|
210
460
|
default:
|
|
211
461
|
throw new Error(`Unsupported update operator: ${operator}`);
|
|
212
462
|
}
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Simple sync manager for primary nodes in JoystickDB.
|
|
3
|
+
* Provides basic TCP synchronization of write operations to secondary nodes
|
|
4
|
+
* with API_KEY authentication and simple retry logic.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import net from 'net';
|
|
8
|
+
import { get_settings } from './load_settings.js';
|
|
9
|
+
import { encode_message } from './tcp_protocol.js';
|
|
10
|
+
import create_logger from './logger.js';
|
|
11
|
+
|
|
12
|
+
const { create_context_logger } = create_logger('simple_sync');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Simple sync manager that syncs write operations to secondary nodes.
|
|
16
|
+
* Uses API_KEY authentication and basic TCP connections with retry logic.
|
|
17
|
+
*/
|
|
18
|
+
class SimpleSyncManager {
|
|
19
|
+
/**
|
|
20
|
+
* Creates a new SimpleSyncManager instance.
|
|
21
|
+
*/
|
|
22
|
+
constructor() {
|
|
23
|
+
/** @type {boolean} Whether this node is configured as primary */
|
|
24
|
+
this.is_primary = false;
|
|
25
|
+
|
|
26
|
+
/** @type {Array<Object>} List of secondary nodes to sync to */
|
|
27
|
+
this.secondary_nodes = [];
|
|
28
|
+
|
|
29
|
+
/** @type {Map<string, Object>} Active connections to secondary nodes */
|
|
30
|
+
this.connections = new Map();
|
|
31
|
+
|
|
32
|
+
/** @type {number} TCP port for sync operations */
|
|
33
|
+
this.sync_port = 1985;
|
|
34
|
+
|
|
35
|
+
/** @type {number} Timeout for sync operations in milliseconds */
|
|
36
|
+
this.sync_timeout_ms = 5000;
|
|
37
|
+
|
|
38
|
+
/** @type {number} Number of retry attempts */
|
|
39
|
+
this.sync_retries = 2;
|
|
40
|
+
|
|
41
|
+
/** @type {number} Sequence number for operations */
|
|
42
|
+
this.sequence_number = 0;
|
|
43
|
+
|
|
44
|
+
/** @type {Object} Logger instance */
|
|
45
|
+
this.log = create_context_logger();
|
|
46
|
+
|
|
47
|
+
/** @type {Object} Sync statistics */
|
|
48
|
+
this.stats = {
|
|
49
|
+
total_synced: 0,
|
|
50
|
+
successful_syncs: 0,
|
|
51
|
+
failed_syncs: 0,
|
|
52
|
+
auth_failures: 0
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initializes the sync manager with settings configuration.
|
|
58
|
+
*/
|
|
59
|
+
initialize() {
|
|
60
|
+
try {
|
|
61
|
+
const settings = get_settings();
|
|
62
|
+
|
|
63
|
+
if (!settings.primary) {
|
|
64
|
+
this.log.info('Node not configured as primary - sync disabled');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.is_primary = settings.primary;
|
|
69
|
+
this.secondary_nodes = settings.secondary_nodes || [];
|
|
70
|
+
this.sync_port = settings.sync_port || 1985;
|
|
71
|
+
this.sync_timeout_ms = settings.sync_timeout_ms || 5000;
|
|
72
|
+
this.sync_retries = settings.sync_retries || 2;
|
|
73
|
+
|
|
74
|
+
this.log.info('Initializing simple sync manager', {
|
|
75
|
+
is_primary: this.is_primary,
|
|
76
|
+
secondary_count: this.secondary_nodes.length,
|
|
77
|
+
sync_port: this.sync_port,
|
|
78
|
+
timeout_ms: this.sync_timeout_ms,
|
|
79
|
+
retries: this.sync_retries
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.connect_to_secondaries();
|
|
83
|
+
|
|
84
|
+
} catch (error) {
|
|
85
|
+
this.log.warn('Could not initialize sync manager - settings not loaded', {
|
|
86
|
+
error: error.message
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Establishes connections to all secondary nodes.
|
|
93
|
+
*/
|
|
94
|
+
connect_to_secondaries() {
|
|
95
|
+
for (const secondary of this.secondary_nodes) {
|
|
96
|
+
this.connect_to_secondary(secondary);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Connects to a single secondary node.
|
|
102
|
+
* @param {Object} secondary - Secondary node configuration
|
|
103
|
+
*/
|
|
104
|
+
connect_to_secondary(secondary) {
|
|
105
|
+
const { ip } = secondary;
|
|
106
|
+
const connection_id = `${ip}:${this.sync_port}`;
|
|
107
|
+
|
|
108
|
+
if (this.connections.has(connection_id)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.log.info('Connecting to secondary node', { ip, port: this.sync_port });
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const socket = new net.Socket();
|
|
116
|
+
|
|
117
|
+
socket.connect(this.sync_port, ip, () => {
|
|
118
|
+
this.log.info('Connected to secondary node', { ip, port: this.sync_port });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
socket.on('error', (error) => {
|
|
122
|
+
this.log.error('Secondary connection error', {
|
|
123
|
+
ip,
|
|
124
|
+
error: error.message
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
this.connections.delete(connection_id);
|
|
128
|
+
|
|
129
|
+
// Retry connection after delay
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
try {
|
|
132
|
+
this.connect_to_secondary(secondary);
|
|
133
|
+
} catch (retry_error) {
|
|
134
|
+
this.log.error('Failed to retry secondary connection', {
|
|
135
|
+
ip,
|
|
136
|
+
error: retry_error.message
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}, 5000);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
socket.on('close', () => {
|
|
143
|
+
this.log.warn('Secondary connection closed', { ip });
|
|
144
|
+
this.connections.delete(connection_id);
|
|
145
|
+
|
|
146
|
+
// Retry connection after delay
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
try {
|
|
149
|
+
this.connect_to_secondary(secondary);
|
|
150
|
+
} catch (retry_error) {
|
|
151
|
+
this.log.error('Failed to retry secondary connection', {
|
|
152
|
+
ip,
|
|
153
|
+
error: retry_error.message
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}, 5000);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
socket.on('data', (data) => {
|
|
160
|
+
try {
|
|
161
|
+
const message = JSON.parse(data.toString());
|
|
162
|
+
this.handle_sync_response(connection_id, message);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
this.log.error('Failed to parse sync response', {
|
|
165
|
+
connection_id,
|
|
166
|
+
error: error.message
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
this.connections.set(connection_id, {
|
|
172
|
+
socket,
|
|
173
|
+
ip,
|
|
174
|
+
connected: true,
|
|
175
|
+
last_sync: null
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
} catch (error) {
|
|
179
|
+
this.log.error('Failed to connect to secondary', {
|
|
180
|
+
ip,
|
|
181
|
+
error: error.message
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Handles sync response from secondary node.
|
|
188
|
+
* @param {string} connection_id - Connection identifier
|
|
189
|
+
* @param {Object} message - Response message from secondary
|
|
190
|
+
*/
|
|
191
|
+
handle_sync_response(connection_id, message) {
|
|
192
|
+
const { type, status, sequence, error } = message;
|
|
193
|
+
|
|
194
|
+
if (type !== 'sync_acknowledged') {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (status === 'success') {
|
|
199
|
+
this.stats.successful_syncs++;
|
|
200
|
+
this.log.debug('Sync acknowledged', { connection_id, sequence });
|
|
201
|
+
} else if (status === 'auth_failed') {
|
|
202
|
+
this.stats.auth_failures++;
|
|
203
|
+
this.log.error('Sync authentication failed', { connection_id, sequence, error });
|
|
204
|
+
} else {
|
|
205
|
+
this.stats.failed_syncs++;
|
|
206
|
+
this.log.error('Sync failed', { connection_id, sequence, error });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Queues an operation for sync to secondary nodes.
|
|
212
|
+
* @param {string} operation - Operation type
|
|
213
|
+
* @param {string} collection - Collection name
|
|
214
|
+
* @param {Object} data - Operation data
|
|
215
|
+
*/
|
|
216
|
+
queue_sync(operation, collection, data) {
|
|
217
|
+
if (!this.is_primary || this.connections.size === 0) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const sequence = ++this.sequence_number;
|
|
222
|
+
|
|
223
|
+
this.log.debug('Queuing sync operation', {
|
|
224
|
+
operation,
|
|
225
|
+
collection,
|
|
226
|
+
sequence,
|
|
227
|
+
secondary_count: this.connections.size
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
this.stats.total_synced++;
|
|
231
|
+
|
|
232
|
+
// Send immediately to all connected secondaries
|
|
233
|
+
this.send_sync_to_secondaries(operation, collection, data, sequence);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Sends sync message to all connected secondary nodes.
|
|
238
|
+
* @param {string} operation - Operation type
|
|
239
|
+
* @param {string} collection - Collection name
|
|
240
|
+
* @param {Object} data - Operation data
|
|
241
|
+
* @param {number} sequence - Sequence number
|
|
242
|
+
*/
|
|
243
|
+
send_sync_to_secondaries(operation, collection, data, sequence) {
|
|
244
|
+
try {
|
|
245
|
+
const settings = get_settings();
|
|
246
|
+
|
|
247
|
+
if (!settings.api_key) {
|
|
248
|
+
this.log.error('No API_KEY configured for sync operations');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const sync_message = {
|
|
253
|
+
type: 'operation_sync',
|
|
254
|
+
api_key: settings.api_key,
|
|
255
|
+
sequence,
|
|
256
|
+
timestamp: Date.now(),
|
|
257
|
+
operation,
|
|
258
|
+
collection,
|
|
259
|
+
data
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const encoded_message = encode_message(sync_message);
|
|
263
|
+
|
|
264
|
+
for (const [connection_id, connection] of this.connections) {
|
|
265
|
+
if (!connection.connected || !connection.socket) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
connection.socket.write(encoded_message);
|
|
271
|
+
connection.last_sync = Date.now();
|
|
272
|
+
|
|
273
|
+
this.log.debug('Sent sync to secondary', {
|
|
274
|
+
connection_id,
|
|
275
|
+
operation,
|
|
276
|
+
sequence
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
} catch (error) {
|
|
280
|
+
this.log.error('Failed to send sync to secondary', {
|
|
281
|
+
connection_id,
|
|
282
|
+
error: error.message
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} catch (error) {
|
|
287
|
+
this.log.error('Failed to send sync to secondaries', {
|
|
288
|
+
operation,
|
|
289
|
+
sequence,
|
|
290
|
+
error: error.message
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Updates the secondary nodes configuration at runtime.
|
|
297
|
+
* @param {Array<Object>} new_secondary_nodes - New secondary nodes list
|
|
298
|
+
*/
|
|
299
|
+
update_secondary_nodes(new_secondary_nodes) {
|
|
300
|
+
this.log.info('Updating secondary nodes configuration', {
|
|
301
|
+
old_count: this.secondary_nodes.length,
|
|
302
|
+
new_count: new_secondary_nodes.length
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Close existing connections
|
|
306
|
+
for (const [connection_id, connection] of this.connections) {
|
|
307
|
+
try {
|
|
308
|
+
connection.socket.end();
|
|
309
|
+
} catch (error) {
|
|
310
|
+
this.log.warn('Error closing secondary connection', {
|
|
311
|
+
connection_id,
|
|
312
|
+
error: error.message
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.connections.clear();
|
|
318
|
+
this.secondary_nodes = new_secondary_nodes;
|
|
319
|
+
|
|
320
|
+
// Establish new connections
|
|
321
|
+
this.connect_to_secondaries();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Forces sync of all connected secondaries (health check).
|
|
326
|
+
* @returns {Promise<Object>} Sync status for each secondary
|
|
327
|
+
*/
|
|
328
|
+
async force_sync() {
|
|
329
|
+
if (!this.is_primary) {
|
|
330
|
+
throw new Error('Node is not configured as primary');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const results = [];
|
|
334
|
+
|
|
335
|
+
for (const [connection_id, connection] of this.connections) {
|
|
336
|
+
try {
|
|
337
|
+
if (connection.connected) {
|
|
338
|
+
results.push({
|
|
339
|
+
connection_id,
|
|
340
|
+
status: 'sync_initiated'
|
|
341
|
+
});
|
|
342
|
+
} else {
|
|
343
|
+
results.push({
|
|
344
|
+
connection_id,
|
|
345
|
+
status: 'not_connected'
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
} catch (error) {
|
|
349
|
+
results.push({
|
|
350
|
+
connection_id,
|
|
351
|
+
status: 'error',
|
|
352
|
+
error: error.message
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
success: true,
|
|
359
|
+
message: 'Force sync initiated',
|
|
360
|
+
results
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Gets current sync status and statistics.
|
|
366
|
+
* @returns {Object} Sync status information
|
|
367
|
+
*/
|
|
368
|
+
get_sync_status() {
|
|
369
|
+
const secondary_status = [];
|
|
370
|
+
|
|
371
|
+
for (const [connection_id, connection] of this.connections) {
|
|
372
|
+
secondary_status.push({
|
|
373
|
+
connection_id,
|
|
374
|
+
ip: connection.ip,
|
|
375
|
+
connected: connection.connected,
|
|
376
|
+
last_sync: connection.last_sync
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
is_primary: this.is_primary,
|
|
382
|
+
secondary_count: this.connections.size,
|
|
383
|
+
stats: this.stats,
|
|
384
|
+
secondaries: secondary_status
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Shuts down the sync manager and closes all connections.
|
|
390
|
+
* @returns {Promise<void>}
|
|
391
|
+
*/
|
|
392
|
+
async shutdown() {
|
|
393
|
+
this.log.info('Shutting down simple sync manager');
|
|
394
|
+
|
|
395
|
+
for (const [connection_id, connection] of this.connections) {
|
|
396
|
+
try {
|
|
397
|
+
connection.socket.end();
|
|
398
|
+
} catch (error) {
|
|
399
|
+
this.log.warn('Error closing secondary connection during shutdown', {
|
|
400
|
+
connection_id,
|
|
401
|
+
error: error.message
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
this.connections.clear();
|
|
407
|
+
this.is_primary = false;
|
|
408
|
+
|
|
409
|
+
this.log.info('Simple sync manager shutdown complete');
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** @type {SimpleSyncManager|null} Singleton instance */
|
|
414
|
+
let sync_manager_instance = null;
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Gets the sync manager singleton instance.
|
|
418
|
+
* @returns {SimpleSyncManager} Sync manager instance
|
|
419
|
+
*/
|
|
420
|
+
export const get_simple_sync_manager = () => {
|
|
421
|
+
if (!sync_manager_instance) {
|
|
422
|
+
sync_manager_instance = new SimpleSyncManager();
|
|
423
|
+
}
|
|
424
|
+
return sync_manager_instance;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Initializes the sync manager singleton.
|
|
429
|
+
*/
|
|
430
|
+
export const initialize_simple_sync_manager = () => {
|
|
431
|
+
const manager = get_simple_sync_manager();
|
|
432
|
+
manager.initialize();
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Shuts down the sync manager singleton.
|
|
437
|
+
* @returns {Promise<void>}
|
|
438
|
+
*/
|
|
439
|
+
export const shutdown_simple_sync_manager = async () => {
|
|
440
|
+
if (sync_manager_instance) {
|
|
441
|
+
await sync_manager_instance.shutdown();
|
|
442
|
+
sync_manager_instance = null;
|
|
443
|
+
}
|
|
444
|
+
};
|