@joystick.js/db-canary 0.0.0-canary.2275 → 0.0.0-canary.2276
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -104
- package/debug_test_runner.js +208 -0
- package/dist/server/index.js +1 -1
- package/dist/server/lib/operation_dispatcher.js +1 -1
- package/dist/server/lib/operations/admin.js +1 -1
- package/dist/server/lib/simple_sync_manager.js +1 -0
- package/dist/server/lib/sync_receiver.js +1 -0
- package/full_debug_test_runner.js +197 -0
- package/package.json +2 -2
- package/src/server/index.js +25 -24
- package/src/server/lib/operation_dispatcher.js +16 -10
- package/src/server/lib/operations/admin.js +64 -31
- package/src/server/lib/simple_sync_manager.js +444 -0
- package/src/server/lib/sync_receiver.js +461 -0
- package/tests/server/lib/simple_sync_system.test.js +124 -0
- package/dist/server/lib/replication_manager.js +0 -1
- package/dist/server/lib/write_forwarder.js +0 -1
- package/src/server/lib/replication_manager.js +0 -727
- package/src/server/lib/write_forwarder.js +0 -636
- package/tests/server/lib/replication_manager.test.js +0 -202
- package/tests/server/lib/write_forwarder.test.js +0 -258
|
@@ -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
|
+
};
|