@joystick.js/db-canary 0.0.0-canary.2270 → 0.0.0-canary.2272
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/dist/server/lib/auto_index_manager.js +1 -1
- package/dist/server/lib/bulk_insert_optimizer.js +1 -0
- package/dist/server/lib/memory_efficient_bulk_insert.js +1 -0
- package/dist/server/lib/write_queue.js +1 -1
- package/package.json +10 -4
- package/src/server/lib/auto_index_manager.js +11 -4
- package/src/server/lib/bulk_insert_optimizer.js +559 -0
- package/src/server/lib/memory_efficient_bulk_insert.js +262 -0
- package/src/server/lib/write_queue.js +2 -137
- package/test_runner.js +353 -0
- package/tests/client/index.test.js +3 -1
- package/tests/performance/bulk_insert_1m_test.js +113 -0
- package/tests/performance/bulk_insert_benchmarks.test.js +570 -0
- package/tests/performance/bulk_insert_enterprise_isolated.test.js +469 -0
- package/tests/performance/bulk_insert_enterprise_scale_test.js +216 -0
- package/tests/server/integration/authentication_integration.test.js +3 -1
- package/tests/server/integration/auto_indexing_integration.test.js +1 -1
- package/tests/server/integration/development_mode_authentication.test.js +3 -1
- package/tests/server/integration/production_safety_integration.test.js +3 -1
- package/tests/server/lib/bulk_insert_optimizer.test.js +523 -0
- package/tests/server/lib/operations/admin.test.js +3 -1
- package/dist/server/lib/batched_write_queue.js +0 -1
- package/dist/server/lib/processing_lane.js +0 -1
- package/src/server/lib/batched_write_queue.js +0 -331
- package/src/server/lib/processing_lane.js +0 -417
- package/tests/server/lib/batched_write_queue.test.js +0 -402
- package/tests/server/lib/write_queue_integration.test.js +0 -186
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Comprehensive tests for JoystickDB bulk insert optimizer.
|
|
3
|
+
* Tests performance optimizations, safety guarantees, and concurrent read capabilities.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import test from 'ava';
|
|
7
|
+
import { rmSync, existsSync } from 'fs';
|
|
8
|
+
import { initialize_database, cleanup_database } from '../../../src/server/lib/query_engine.js';
|
|
9
|
+
import {
|
|
10
|
+
bulk_insert_optimized,
|
|
11
|
+
bulk_insert_with_metrics,
|
|
12
|
+
non_blocking_bulk_insert,
|
|
13
|
+
calculate_average_document_size,
|
|
14
|
+
calculate_bulk_map_size,
|
|
15
|
+
create_size_based_batches,
|
|
16
|
+
sort_documents_by_key,
|
|
17
|
+
pre_encode_documents
|
|
18
|
+
} from '../../../src/server/lib/bulk_insert_optimizer.js';
|
|
19
|
+
import find_one from '../../../src/server/lib/operations/find_one.js';
|
|
20
|
+
import find from '../../../src/server/lib/operations/find.js';
|
|
21
|
+
|
|
22
|
+
const TEST_DB_PATH = './test_data/bulk_optimizer_test';
|
|
23
|
+
const TEST_DATABASE = 'test_db';
|
|
24
|
+
const TEST_COLLECTION = 'test_collection';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generates test documents for bulk insert testing.
|
|
28
|
+
* @param {number} count - Number of documents to generate
|
|
29
|
+
* @param {Object} [options={}] - Generation options
|
|
30
|
+
* @returns {Array<Object>} Array of test documents
|
|
31
|
+
*/
|
|
32
|
+
const generate_test_documents = (count, options = {}) => {
|
|
33
|
+
const {
|
|
34
|
+
include_id = false,
|
|
35
|
+
base_size = 100,
|
|
36
|
+
variable_size = false
|
|
37
|
+
} = options;
|
|
38
|
+
|
|
39
|
+
const documents = [];
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < count; i++) {
|
|
42
|
+
const doc = {
|
|
43
|
+
name: `Test Document ${i}`,
|
|
44
|
+
index: i,
|
|
45
|
+
category: `category_${i % 10}`,
|
|
46
|
+
active: i % 2 === 0,
|
|
47
|
+
metadata: {
|
|
48
|
+
created_by: 'test_user',
|
|
49
|
+
tags: [`tag_${i % 5}`, `tag_${(i + 1) % 5}`],
|
|
50
|
+
priority: i % 3
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (include_id) {
|
|
55
|
+
doc._id = `test_doc_${i.toString().padStart(6, '0')}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Add variable size content for testing
|
|
59
|
+
if (variable_size) {
|
|
60
|
+
const extra_content_size = Math.floor(Math.random() * base_size);
|
|
61
|
+
doc.content = 'x'.repeat(extra_content_size);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
documents.push(doc);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return documents;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Sets up test database before each test.
|
|
72
|
+
*/
|
|
73
|
+
test.beforeEach(async () => {
|
|
74
|
+
// Clean up any existing test database
|
|
75
|
+
if (existsSync(TEST_DB_PATH)) {
|
|
76
|
+
rmSync(TEST_DB_PATH, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Initialize fresh database
|
|
80
|
+
initialize_database(TEST_DB_PATH);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Cleans up test database after each test.
|
|
85
|
+
*/
|
|
86
|
+
test.afterEach(async () => {
|
|
87
|
+
await cleanup_database(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Utility function tests
|
|
91
|
+
test('calculate_average_document_size should calculate correct average', t => {
|
|
92
|
+
const documents = [
|
|
93
|
+
{ name: 'doc1', data: 'small' },
|
|
94
|
+
{ name: 'doc2', data: 'medium_sized_content' },
|
|
95
|
+
{ name: 'doc3', data: 'large_content_with_more_data' }
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const avg_size = calculate_average_document_size(documents);
|
|
99
|
+
|
|
100
|
+
t.true(avg_size > 0);
|
|
101
|
+
t.true(typeof avg_size === 'number');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('calculate_bulk_map_size should return appropriate map size', t => {
|
|
105
|
+
const document_count = 100000;
|
|
106
|
+
const avg_document_size = 500;
|
|
107
|
+
|
|
108
|
+
const map_size = calculate_bulk_map_size(document_count, avg_document_size);
|
|
109
|
+
|
|
110
|
+
// Should be at least 2x the estimated size
|
|
111
|
+
const estimated_size = document_count * avg_document_size;
|
|
112
|
+
t.true(map_size >= estimated_size * 2);
|
|
113
|
+
|
|
114
|
+
// Should be at least 10GB minimum
|
|
115
|
+
const minimum_size = 1024 * 1024 * 1024 * 10;
|
|
116
|
+
t.true(map_size >= minimum_size);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('create_size_based_batches should create appropriate batches', t => {
|
|
120
|
+
const documents = generate_test_documents(1000);
|
|
121
|
+
const target_size = 50 * 1024; // 50KB target
|
|
122
|
+
|
|
123
|
+
const batches = create_size_based_batches(documents, target_size);
|
|
124
|
+
|
|
125
|
+
t.true(batches.length > 0);
|
|
126
|
+
t.true(Array.isArray(batches));
|
|
127
|
+
|
|
128
|
+
// Verify all documents are included
|
|
129
|
+
const total_docs = batches.reduce((sum, batch) => sum + batch.length, 0);
|
|
130
|
+
t.is(total_docs, documents.length);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('sort_documents_by_key should sort documents correctly', t => {
|
|
134
|
+
const documents = generate_test_documents(100);
|
|
135
|
+
|
|
136
|
+
const sorted_docs = sort_documents_by_key(documents, TEST_DATABASE, TEST_COLLECTION);
|
|
137
|
+
|
|
138
|
+
t.is(sorted_docs.length, documents.length);
|
|
139
|
+
|
|
140
|
+
// Verify all documents have IDs
|
|
141
|
+
sorted_docs.forEach(doc => {
|
|
142
|
+
t.truthy(doc._id);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Verify sorting (keys should be in ascending order)
|
|
146
|
+
for (let i = 1; i < sorted_docs.length; i++) {
|
|
147
|
+
const key_a = `${TEST_DATABASE}:${TEST_COLLECTION}:${sorted_docs[i-1]._id}`;
|
|
148
|
+
const key_b = `${TEST_DATABASE}:${TEST_COLLECTION}:${sorted_docs[i]._id}`;
|
|
149
|
+
t.true(key_a.localeCompare(key_b) <= 0);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('pre_encode_documents should encode documents correctly', t => {
|
|
154
|
+
const documents = generate_test_documents(10, { include_id: true });
|
|
155
|
+
|
|
156
|
+
const encoded_docs = pre_encode_documents(documents, TEST_DATABASE, TEST_COLLECTION);
|
|
157
|
+
|
|
158
|
+
t.is(encoded_docs.length, documents.length);
|
|
159
|
+
|
|
160
|
+
encoded_docs.forEach((encoded, index) => {
|
|
161
|
+
t.truthy(encoded.key);
|
|
162
|
+
t.truthy(encoded.value);
|
|
163
|
+
t.truthy(encoded.document_id);
|
|
164
|
+
t.is(encoded.document_id, documents[index]._id);
|
|
165
|
+
|
|
166
|
+
// Verify key format
|
|
167
|
+
t.true(encoded.key.startsWith(`${TEST_DATABASE}:${TEST_COLLECTION}:`));
|
|
168
|
+
|
|
169
|
+
// Verify value is valid JSON
|
|
170
|
+
const parsed = JSON.parse(encoded.value);
|
|
171
|
+
t.truthy(parsed._created_at);
|
|
172
|
+
t.truthy(parsed._updated_at);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Basic bulk insert tests
|
|
177
|
+
test('bulk_insert_optimized should insert small dataset successfully', async t => {
|
|
178
|
+
const documents = generate_test_documents(100);
|
|
179
|
+
|
|
180
|
+
const result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, documents);
|
|
181
|
+
|
|
182
|
+
t.true(result.acknowledged);
|
|
183
|
+
t.is(result.inserted_count, 100);
|
|
184
|
+
t.is(result.inserted_ids.length, 100);
|
|
185
|
+
t.truthy(result.performance);
|
|
186
|
+
t.true(result.performance.duration_ms > 0);
|
|
187
|
+
t.true(result.performance.documents_per_second > 0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('bulk_insert_optimized should insert medium dataset successfully', async t => {
|
|
191
|
+
const documents = generate_test_documents(10000);
|
|
192
|
+
|
|
193
|
+
const result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, documents);
|
|
194
|
+
|
|
195
|
+
t.true(result.acknowledged);
|
|
196
|
+
t.is(result.inserted_count, 10000);
|
|
197
|
+
t.is(result.inserted_ids.length, 10000);
|
|
198
|
+
t.truthy(result.performance);
|
|
199
|
+
|
|
200
|
+
// Performance should be reasonable
|
|
201
|
+
t.true(result.performance.documents_per_second > 1000);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('bulk_insert_optimized should handle large dataset efficiently', async t => {
|
|
205
|
+
const documents = generate_test_documents(100000);
|
|
206
|
+
|
|
207
|
+
const start_time = Date.now();
|
|
208
|
+
const result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, documents);
|
|
209
|
+
const duration = Date.now() - start_time;
|
|
210
|
+
|
|
211
|
+
t.true(result.acknowledged);
|
|
212
|
+
t.is(result.inserted_count, 100000);
|
|
213
|
+
t.is(result.inserted_ids.length, 100000);
|
|
214
|
+
|
|
215
|
+
// Should complete in reasonable time (less than 30 seconds)
|
|
216
|
+
t.true(duration < 30000);
|
|
217
|
+
|
|
218
|
+
// Should achieve good throughput
|
|
219
|
+
t.true(result.performance.documents_per_second > 3000);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Performance optimization tests
|
|
223
|
+
test('bulk_insert_optimized with streaming should handle memory efficiently', async t => {
|
|
224
|
+
const documents = generate_test_documents(50000, { variable_size: true, base_size: 1000 });
|
|
225
|
+
|
|
226
|
+
const start_memory = process.memoryUsage().heapUsed;
|
|
227
|
+
|
|
228
|
+
const result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, documents, {
|
|
229
|
+
stream_processing: true,
|
|
230
|
+
batch_size: 1000
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const end_memory = process.memoryUsage().heapUsed;
|
|
234
|
+
const memory_delta = end_memory - start_memory;
|
|
235
|
+
|
|
236
|
+
t.true(result.acknowledged);
|
|
237
|
+
t.is(result.inserted_count, 50000);
|
|
238
|
+
|
|
239
|
+
// Memory usage should be reasonable (less than 500MB delta)
|
|
240
|
+
t.true(memory_delta < 500 * 1024 * 1024);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('bulk_insert_optimized with key sorting should improve performance', async t => {
|
|
244
|
+
const documents = generate_test_documents(10000);
|
|
245
|
+
|
|
246
|
+
// Test with sorting enabled
|
|
247
|
+
const sorted_result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, documents, {
|
|
248
|
+
sort_keys: true
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Clean up for second test
|
|
252
|
+
await cleanup_database(true);
|
|
253
|
+
initialize_database(TEST_DB_PATH);
|
|
254
|
+
|
|
255
|
+
// Test with sorting disabled
|
|
256
|
+
const unsorted_result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, documents, {
|
|
257
|
+
sort_keys: false
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
t.true(sorted_result.acknowledged);
|
|
261
|
+
t.true(unsorted_result.acknowledged);
|
|
262
|
+
t.is(sorted_result.inserted_count, 10000);
|
|
263
|
+
t.is(unsorted_result.inserted_count, 10000);
|
|
264
|
+
|
|
265
|
+
// Sorted version should generally be faster or similar
|
|
266
|
+
// (This is a performance hint, not a strict requirement)
|
|
267
|
+
t.true(sorted_result.performance.duration_ms <= unsorted_result.performance.duration_ms * 1.5);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Error handling tests
|
|
271
|
+
test('bulk_insert_optimized should reject invalid parameters', async t => {
|
|
272
|
+
await t.throwsAsync(
|
|
273
|
+
() => bulk_insert_optimized('', TEST_COLLECTION, []),
|
|
274
|
+
{ message: /Database name and collection name are required/ }
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
await t.throwsAsync(
|
|
278
|
+
() => bulk_insert_optimized(TEST_DATABASE, '', []),
|
|
279
|
+
{ message: /Database name and collection name are required/ }
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
await t.throwsAsync(
|
|
283
|
+
() => bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, []),
|
|
284
|
+
{ message: /Documents must be a non-empty array/ }
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
await t.throwsAsync(
|
|
288
|
+
() => bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, null),
|
|
289
|
+
{ message: /Documents must be a non-empty array/ }
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('bulk_insert_optimized should handle duplicate IDs correctly', async t => {
|
|
294
|
+
const documents = [
|
|
295
|
+
{ _id: 'duplicate_id', name: 'Document 1' },
|
|
296
|
+
{ _id: 'duplicate_id', name: 'Document 2' }
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
await t.throwsAsync(
|
|
300
|
+
() => bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, documents),
|
|
301
|
+
{ message: /Document with _id duplicate_id already exists/ }
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Data integrity tests
|
|
306
|
+
test('bulk_insert_optimized should preserve document data integrity', async t => {
|
|
307
|
+
const original_documents = generate_test_documents(1000, { include_id: true });
|
|
308
|
+
|
|
309
|
+
await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, original_documents);
|
|
310
|
+
|
|
311
|
+
// Verify all documents were inserted correctly
|
|
312
|
+
for (const original_doc of original_documents) {
|
|
313
|
+
const retrieved_doc = await find_one(TEST_DATABASE, TEST_COLLECTION, { _id: original_doc._id });
|
|
314
|
+
|
|
315
|
+
t.truthy(retrieved_doc);
|
|
316
|
+
t.is(retrieved_doc.name, original_doc.name);
|
|
317
|
+
t.is(retrieved_doc.index, original_doc.index);
|
|
318
|
+
t.is(retrieved_doc.category, original_doc.category);
|
|
319
|
+
t.is(retrieved_doc.active, original_doc.active);
|
|
320
|
+
t.deepEqual(retrieved_doc.metadata, original_doc.metadata);
|
|
321
|
+
t.truthy(retrieved_doc._created_at);
|
|
322
|
+
t.truthy(retrieved_doc._updated_at);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Concurrent read safety tests
|
|
327
|
+
test('bulk_insert_optimized should allow concurrent reads', async t => {
|
|
328
|
+
// Insert initial data
|
|
329
|
+
const initial_docs = generate_test_documents(1000, { include_id: true });
|
|
330
|
+
await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, initial_docs);
|
|
331
|
+
|
|
332
|
+
// Start bulk insert of additional data
|
|
333
|
+
const additional_docs = generate_test_documents(10000);
|
|
334
|
+
const bulk_insert_promise = bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, additional_docs);
|
|
335
|
+
|
|
336
|
+
// Perform concurrent reads while bulk insert is running
|
|
337
|
+
const read_promises = [];
|
|
338
|
+
for (let i = 0; i < 10; i++) {
|
|
339
|
+
read_promises.push(
|
|
340
|
+
find_one(TEST_DATABASE, TEST_COLLECTION, { _id: initial_docs[i]._id })
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Wait for both bulk insert and reads to complete
|
|
345
|
+
const [bulk_result, ...read_results] = await Promise.all([
|
|
346
|
+
bulk_insert_promise,
|
|
347
|
+
...read_promises
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
// Verify bulk insert succeeded
|
|
351
|
+
t.true(bulk_result.acknowledged);
|
|
352
|
+
t.is(bulk_result.inserted_count, 10000);
|
|
353
|
+
|
|
354
|
+
// Verify all reads succeeded and returned correct data
|
|
355
|
+
read_results.forEach((doc, index) => {
|
|
356
|
+
t.truthy(doc);
|
|
357
|
+
t.is(doc._id, initial_docs[index]._id);
|
|
358
|
+
t.is(doc.name, initial_docs[index].name);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Performance monitoring tests
|
|
363
|
+
test('bulk_insert_with_metrics should provide detailed performance metrics', async t => {
|
|
364
|
+
const documents = generate_test_documents(5000);
|
|
365
|
+
|
|
366
|
+
const result = await bulk_insert_with_metrics(TEST_DATABASE, TEST_COLLECTION, documents);
|
|
367
|
+
|
|
368
|
+
t.true(result.acknowledged);
|
|
369
|
+
t.is(result.inserted_count, 5000);
|
|
370
|
+
t.truthy(result.performance);
|
|
371
|
+
t.truthy(result.performance.memory_usage);
|
|
372
|
+
|
|
373
|
+
// Verify performance metrics structure
|
|
374
|
+
t.true(typeof result.performance.duration_ms === 'number');
|
|
375
|
+
t.true(typeof result.performance.documents_per_second === 'number');
|
|
376
|
+
t.true(typeof result.performance.memory_usage.start_heap_mb === 'number');
|
|
377
|
+
t.true(typeof result.performance.memory_usage.end_heap_mb === 'number');
|
|
378
|
+
t.true(typeof result.performance.memory_usage.delta_heap_mb === 'number');
|
|
379
|
+
t.true(typeof result.performance.memory_usage.peak_heap_mb === 'number');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Non-blocking bulk insert tests
|
|
383
|
+
test('non_blocking_bulk_insert should process in chunks', async t => {
|
|
384
|
+
const documents = generate_test_documents(25000);
|
|
385
|
+
|
|
386
|
+
const result = await non_blocking_bulk_insert(TEST_DATABASE, TEST_COLLECTION, documents, {
|
|
387
|
+
chunk_size: 5000
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
t.true(result.acknowledged);
|
|
391
|
+
t.is(result.inserted_count, 25000);
|
|
392
|
+
t.is(result.inserted_ids.length, 25000);
|
|
393
|
+
t.truthy(result.performance);
|
|
394
|
+
t.true(result.performance.duration_ms > 0);
|
|
395
|
+
t.true(result.performance.documents_per_second > 0);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Stress tests
|
|
399
|
+
test('bulk_insert_optimized should handle very large documents', async t => {
|
|
400
|
+
const large_documents = [];
|
|
401
|
+
for (let i = 0; i < 1000; i++) {
|
|
402
|
+
large_documents.push({
|
|
403
|
+
name: `Large Document ${i}`,
|
|
404
|
+
large_content: 'x'.repeat(10000), // 10KB per document
|
|
405
|
+
nested_data: {
|
|
406
|
+
level1: {
|
|
407
|
+
level2: {
|
|
408
|
+
level3: {
|
|
409
|
+
data: Array.from({ length: 100 }, (_, j) => ({ id: j, value: `value_${j}` }))
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, large_documents);
|
|
418
|
+
|
|
419
|
+
t.true(result.acknowledged);
|
|
420
|
+
t.is(result.inserted_count, 1000);
|
|
421
|
+
t.true(result.performance.documents_per_second > 100);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test('bulk_insert_optimized should maintain performance with mixed document sizes', async t => {
|
|
425
|
+
const mixed_documents = [];
|
|
426
|
+
|
|
427
|
+
// Small documents
|
|
428
|
+
for (let i = 0; i < 5000; i++) {
|
|
429
|
+
mixed_documents.push({
|
|
430
|
+
type: 'small',
|
|
431
|
+
index: i,
|
|
432
|
+
data: 'small_content'
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Medium documents
|
|
437
|
+
for (let i = 0; i < 3000; i++) {
|
|
438
|
+
mixed_documents.push({
|
|
439
|
+
type: 'medium',
|
|
440
|
+
index: i,
|
|
441
|
+
data: 'x'.repeat(1000),
|
|
442
|
+
metadata: Array.from({ length: 50 }, (_, j) => ({ key: `key_${j}`, value: `value_${j}` }))
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Large documents
|
|
447
|
+
for (let i = 0; i < 2000; i++) {
|
|
448
|
+
mixed_documents.push({
|
|
449
|
+
type: 'large',
|
|
450
|
+
index: i,
|
|
451
|
+
data: 'x'.repeat(5000),
|
|
452
|
+
large_array: Array.from({ length: 200 }, (_, j) => ({ id: j, content: `content_${j}` }))
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, mixed_documents);
|
|
457
|
+
|
|
458
|
+
t.true(result.acknowledged);
|
|
459
|
+
t.is(result.inserted_count, 10000);
|
|
460
|
+
t.true(result.performance.documents_per_second > 2000);
|
|
461
|
+
|
|
462
|
+
// Verify data integrity for different document types
|
|
463
|
+
const small_doc = await find_one(TEST_DATABASE, TEST_COLLECTION, { type: 'small' });
|
|
464
|
+
const medium_doc = await find_one(TEST_DATABASE, TEST_COLLECTION, { type: 'medium' });
|
|
465
|
+
const large_doc = await find_one(TEST_DATABASE, TEST_COLLECTION, { type: 'large' });
|
|
466
|
+
|
|
467
|
+
t.truthy(small_doc);
|
|
468
|
+
t.is(small_doc.type, 'small');
|
|
469
|
+
t.truthy(medium_doc);
|
|
470
|
+
t.is(medium_doc.type, 'medium');
|
|
471
|
+
t.is(medium_doc.metadata.length, 50);
|
|
472
|
+
t.truthy(large_doc);
|
|
473
|
+
t.is(large_doc.type, 'large');
|
|
474
|
+
t.is(large_doc.large_array.length, 200);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Edge case tests
|
|
478
|
+
test('bulk_insert_optimized should handle documents with special characters', async t => {
|
|
479
|
+
const special_documents = [
|
|
480
|
+
{ name: 'Document with "quotes"', content: 'Content with \n newlines and \t tabs' },
|
|
481
|
+
{ name: 'Document with émojis 🚀', content: 'Unicode content: café, naïve, résumé' },
|
|
482
|
+
{ name: 'Document with JSON', content: '{"nested": "json", "array": [1, 2, 3]}' },
|
|
483
|
+
{ name: 'Document with HTML', content: '<div>HTML content</div>' },
|
|
484
|
+
{ name: 'Document with null values', nullable_field: null, undefined_field: undefined }
|
|
485
|
+
];
|
|
486
|
+
|
|
487
|
+
const result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, special_documents);
|
|
488
|
+
|
|
489
|
+
t.true(result.acknowledged);
|
|
490
|
+
t.is(result.inserted_count, 5);
|
|
491
|
+
|
|
492
|
+
// Verify special characters are preserved
|
|
493
|
+
const docs = await find(TEST_DATABASE, TEST_COLLECTION, {});
|
|
494
|
+
t.is(docs.length, 5);
|
|
495
|
+
|
|
496
|
+
const emoji_doc = docs.find(doc => doc.name.includes('émojis'));
|
|
497
|
+
t.truthy(emoji_doc);
|
|
498
|
+
t.true(emoji_doc.content.includes('café'));
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test('bulk_insert_optimized should handle empty and minimal documents', async t => {
|
|
502
|
+
const minimal_documents = [
|
|
503
|
+
{},
|
|
504
|
+
{ single_field: 'value' },
|
|
505
|
+
{ _id: 'custom_id_1' },
|
|
506
|
+
{ _id: 'custom_id_2', empty_object: {}, empty_array: [] }
|
|
507
|
+
];
|
|
508
|
+
|
|
509
|
+
const result = await bulk_insert_optimized(TEST_DATABASE, TEST_COLLECTION, minimal_documents);
|
|
510
|
+
|
|
511
|
+
t.true(result.acknowledged);
|
|
512
|
+
t.is(result.inserted_count, 4);
|
|
513
|
+
|
|
514
|
+
// Verify all documents were inserted with proper timestamps
|
|
515
|
+
const docs = await find(TEST_DATABASE, TEST_COLLECTION, {});
|
|
516
|
+
t.is(docs.length, 4);
|
|
517
|
+
|
|
518
|
+
docs.forEach(doc => {
|
|
519
|
+
t.truthy(doc._id);
|
|
520
|
+
t.truthy(doc._created_at);
|
|
521
|
+
t.truthy(doc._updated_at);
|
|
522
|
+
});
|
|
523
|
+
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import l from"./processing_lane.js";import d from"./logger.js";const{create_context_logger:p}=d("batched_write_queue");class h{constructor(t={}){this.batch_size=t.batch_size||100,this.batch_timeout=t.batch_timeout||10,this.lane_count=t.lane_count||4,this.queue_limit=t.queue_limit||1e4,this.overflow_strategy=t.overflow_strategy||"block",this.lanes=Array(this.lane_count).fill(null).map((s,o)=>new l({batch_size:this.batch_size,batch_timeout:this.batch_timeout,lane_id:o})),this.shutting_down=!1,this.stats={total_operations:0,completed_operations:0,failed_operations:0,current_queue_depth:0,max_queue_depth:0,total_wait_time_ms:0,total_processing_time_ms:0,lane_distribution:new Array(this.lane_count).fill(0)},this.log=p()}async enqueue_write_operation(t,s={}){if(this.shutting_down)throw new Error("Server shutting down");if(this.get_current_queue_depth()>=this.queue_limit){if(this.overflow_strategy==="drop")throw new Error("Queue full, operation dropped");this.overflow_strategy==="block"&&await this.wait_for_queue_space()}const i={operation_fn:t,context:s,enqueued_at:Date.now()},_=this.get_lane_for_operation(i),e=this.lanes[_];this.stats.total_operations++,this.stats.lane_distribution[_]++,this.update_queue_depth_stats(),this.log.debug("Operation enqueued to lane",{lane_id:_,total_operations:this.stats.total_operations,context:s});try{const a=await e.add_operation(i);this.stats.completed_operations++;const r=Date.now()-i.enqueued_at;return this.stats.total_wait_time_ms+=r,a}catch(a){throw this.stats.failed_operations++,a}}get_lane_for_operation(t){const s=t.context||{},o=s.collection||"",i=s.document_id||s.id||"",_=`${o}:${i}`;let e=0;for(let r=0;r<_.length;r++){const c=_.charCodeAt(r);e=(e<<5)-e+c,e=e&e}return Math.abs(e)%this.lane_count}get_current_queue_depth(){return this.lanes.reduce((t,s)=>t+s.stats.current_batch_size,0)}update_queue_depth_stats(){this.stats.current_queue_depth=this.get_current_queue_depth(),this.stats.current_queue_depth>this.stats.max_queue_depth&&(this.stats.max_queue_depth=this.stats.current_queue_depth)}async wait_for_queue_space(){const o=Date.now();for(;this.get_current_queue_depth()>=this.queue_limit;){if(Date.now()-o>5e3)throw new Error("Queue full, timeout waiting for space");if(await new Promise(i=>setTimeout(i,10)),this.shutting_down)throw new Error("Server shutting down")}}async flush_all_batches(){const t=this.lanes.map(s=>s.flush_batch());await Promise.all(t)}get_stats(){const t=this.lanes.map(e=>e.get_stats()),s=this.stats.completed_operations>0?Math.round(this.stats.total_wait_time_ms/this.stats.completed_operations):0,o=t.reduce((e,a)=>e+a.total_batch_processing_time_ms,0),i=this.stats.completed_operations>0?Math.round(o/this.stats.completed_operations):0,_=this.stats.lane_distribution.map((e,a)=>({lane_id:a,operations:e,percentage:this.stats.total_operations>0?Math.round(e/this.stats.total_operations*100):0}));return{total_operations:this.stats.total_operations,completed_operations:this.stats.completed_operations,failed_operations:this.stats.failed_operations,current_queue_depth:this.get_current_queue_depth(),max_queue_depth:this.stats.max_queue_depth,avg_wait_time_ms:s,avg_processing_time_ms:i,success_rate:this.stats.total_operations>0?Math.round(this.stats.completed_operations/this.stats.total_operations*100):100,lane_count:this.lane_count,batch_size:this.batch_size,batch_timeout:this.batch_timeout,lane_distribution:this.stats.lane_distribution,lane_utilization:_,lane_stats:t,total_batches_processed:t.reduce((e,a)=>e+a.batches_processed,0),avg_batch_size:t.length>0?Math.round(t.reduce((e,a)=>e+a.avg_batch_size,0)/t.length):0}}clear_stats(){this.stats={total_operations:0,completed_operations:0,failed_operations:0,current_queue_depth:this.get_current_queue_depth(),max_queue_depth:0,total_wait_time_ms:0,total_processing_time_ms:0,lane_distribution:new Array(this.lane_count).fill(0)},this.lanes.forEach(t=>t.clear_stats())}async shutdown(){this.log.info("Shutting down batched write queue",{pending_operations:this.get_current_queue_depth(),lane_count:this.lane_count}),this.shutting_down=!0,await this.flush_all_batches();const t=this.lanes.map(s=>s.shutdown());await Promise.all(t),this.log.info("Batched write queue shutdown complete")}}let n=null;const g=u=>(n||(n=new h(u)),n),f=async()=>{n&&(await n.shutdown(),n=null)};var q=h;export{q as default,g as get_batched_write_queue,f as shutdown_batched_write_queue};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import c from"./logger.js";const{create_context_logger:n}=c("processing_lane");class o{constructor(t={}){this.batch_size=t.batch_size||100,this.batch_timeout=t.batch_timeout||10,this.lane_id=t.lane_id||0,this.current_batch=[],this.processing=!1,this.shutting_down=!1,this.batch_timeout_handle=null,this.stats={total_operations:0,completed_operations:0,failed_operations:0,batches_processed:0,current_batch_size:0,max_batch_size:0,total_batch_wait_time_ms:0,total_batch_processing_time_ms:0},this.log=n(`lane_${this.lane_id}`)}async add_operation(t){if(this.shutting_down)throw new Error("Processing lane shutting down");return new Promise((a,s)=>{if(this.shutting_down){s(new Error("Processing lane shutting down"));return}const e={...t,resolve:a,reject:s,enqueued_at:Date.now(),id:this.generate_operation_id()};this.current_batch.push(e),this.stats.total_operations++,this.stats.current_batch_size=this.current_batch.length,this.stats.current_batch_size>this.stats.max_batch_size&&(this.stats.max_batch_size=this.stats.current_batch_size),this.log.debug("Operation added to batch",{lane_id:this.lane_id,operation_id:e.id,batch_size:this.stats.current_batch_size,context:t.context}),this.current_batch.length>=this.batch_size?this.process_current_batch():this.current_batch.length===1&&this.start_batch_timeout()})}start_batch_timeout(){this.batch_timeout_handle&&clearTimeout(this.batch_timeout_handle),this.batch_timeout_handle=setTimeout(()=>{this.current_batch.length>0&&!this.processing&&(this.log.debug("Batch timeout triggered",{lane_id:this.lane_id,batch_size:this.current_batch.length}),this.process_current_batch())},this.batch_timeout)}async process_current_batch(){if(this.processing||this.current_batch.length===0||this.shutting_down)return;this.batch_timeout_handle&&(clearTimeout(this.batch_timeout_handle),this.batch_timeout_handle=null),this.processing=!0;const t=[...this.current_batch];this.current_batch=[],this.stats.current_batch_size=0;const a=Date.now(),s=Math.min(...t.map(i=>i.enqueued_at)),e=a-s;this.stats.total_batch_wait_time_ms+=e,this.stats.batches_processed++,this.log.debug("Processing batch",{lane_id:this.lane_id,batch_size:t.length,batch_wait_time_ms:e});try{const i=await this.execute_batch_transaction(t),h=Date.now()-a;this.stats.total_batch_processing_time_ms+=h,this.stats.completed_operations+=t.length,t.forEach((_,r)=>{_.resolve(i[r])}),this.log.debug("Batch completed successfully",{lane_id:this.lane_id,batch_size:t.length,batch_wait_time_ms:e,batch_processing_time_ms:h})}catch(i){const h=Date.now()-a;this.stats.total_batch_processing_time_ms+=h,this.stats.failed_operations+=t.length,t.forEach(_=>{_.reject(i)}),this.log.error("Batch processing failed",{lane_id:this.lane_id,batch_size:t.length,batch_wait_time_ms:e,batch_processing_time_ms:h,error:i.message})}this.processing=!1,this.current_batch.length>0&&(this.current_batch.length>=this.batch_size?setImmediate(()=>this.process_current_batch()):this.start_batch_timeout())}async execute_batch_transaction(t){const a=[];for(const s of t)try{const e=await this.execute_with_retry(s.operation_fn,s.context);a.push(e)}catch(e){throw e}return a}async execute_with_retry(t,a,s=3){let e=null;for(let i=1;i<=s;i++)try{return await t()}catch(h){if(e=h,this.is_retryable_error(h)&&i<s){const _=this.calculate_backoff_delay(i);this.log.warn("Operation failed, retrying",{lane_id:this.lane_id,attempt:i,max_retries:s,delay_ms:_,error:h.message,context:a}),await this.sleep(_);continue}break}throw e}is_retryable_error(t){return["MDB_MAP_FULL","MDB_TXN_FULL","MDB_READERS_FULL","EAGAIN","EBUSY"].some(s=>t.message.includes(s)||t.code===s)}calculate_backoff_delay(t){const e=100*Math.pow(2,t-1),i=Math.random()*.1*e;return Math.min(e+i,5e3)}sleep(t){return new Promise(a=>setTimeout(a,t))}generate_operation_id(){return`lane_${this.lane_id}_${Date.now()}_${Math.random().toString(36).substr(2,9)}`}get_stats(){const t=this.stats.batches_processed>0?Math.round(this.stats.total_batch_wait_time_ms/this.stats.batches_processed):0,a=this.stats.batches_processed>0?Math.round(this.stats.total_batch_processing_time_ms/this.stats.batches_processed):0,s=this.stats.batches_processed>0?Math.round(this.stats.completed_operations/this.stats.batches_processed):0;return{lane_id:this.lane_id,...this.stats,avg_batch_wait_time_ms:t,avg_batch_processing_time_ms:a,avg_batch_size:s,success_rate:this.stats.total_operations>0?Math.round(this.stats.completed_operations/this.stats.total_operations*100):100}}clear_stats(){this.stats={total_operations:0,completed_operations:0,failed_operations:0,batches_processed:0,current_batch_size:this.current_batch.length,max_batch_size:0,total_batch_wait_time_ms:0,total_batch_processing_time_ms:0}}async flush_batch(){this.current_batch.length>0&&!this.processing&&await this.process_current_batch()}async shutdown(){for(this.log.info("Shutting down processing lane",{lane_id:this.lane_id,pending_operations:this.current_batch.length,currently_processing:this.processing}),this.shutting_down=!0,this.batch_timeout_handle&&(clearTimeout(this.batch_timeout_handle),this.batch_timeout_handle=null),this.current_batch.length>0&&!this.processing&&await this.process_current_batch();this.processing;)await new Promise(t=>setTimeout(t,10));this.current_batch.forEach(t=>{t.reject(new Error("Processing lane shutting down"))}),this.current_batch=[],this.processing=!1}}var b=o;export{b as default};
|