bulltrackers-module 1.0.189 → 1.0.191
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FILENAME: bulltrackers-module/functions/computation-system/helpers/computation_pass_runner.js
|
|
3
3
|
* FIXED: Integrates 'runBatchPriceComputation' to prevent OOM on price calculations.
|
|
4
|
+
* FIXED: Added try/catch around runBatchPriceComputation to prevent crash on failure.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
const {
|
|
@@ -65,44 +66,49 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
65
66
|
if (priceBatchCalcs.length > 0) {
|
|
66
67
|
logger.log('INFO', `[PassRunner] Detected ${priceBatchCalcs.length} Price-Meta calculations. Checking statuses...`);
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// Check statuses in chunks to avoid blowing up IO
|
|
73
|
-
const STATUS_CHECK_CHUNK = 20;
|
|
74
|
-
for (let i = 0; i < allExpectedDates.length; i += STATUS_CHECK_CHUNK) {
|
|
75
|
-
const dateChunk = allExpectedDates.slice(i, i + STATUS_CHECK_CHUNK);
|
|
76
|
-
await Promise.all(dateChunk.map(async (dateStr) => {
|
|
77
|
-
const status = await fetchComputationStatus(dateStr, config, dependencies);
|
|
78
|
-
// If ANY of the price calcs are missing/false, we run the batch for this date
|
|
79
|
-
const needsRun = priceBatchCalcs.some(c => status[normalizeName(c.name)] !== true);
|
|
80
|
-
if (needsRun) datesNeedingPriceCalc.push(dateStr);
|
|
81
|
-
}));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (datesNeedingPriceCalc.length > 0) {
|
|
85
|
-
logger.log('INFO', `[PassRunner] >>> Starting Optimized Batch for ${datesNeedingPriceCalc.length} dates <<<`);
|
|
86
|
-
|
|
87
|
-
// Execute the Shard-First Logic
|
|
88
|
-
await runBatchPriceComputation(config, dependencies, datesNeedingPriceCalc, priceBatchCalcs);
|
|
89
|
-
|
|
90
|
-
// Manually update statuses for these dates/calcs upon completion
|
|
91
|
-
// (runBatchPriceComputation handles the results, but we must mark the status doc)
|
|
92
|
-
logger.log('INFO', `[PassRunner] Updating status documents for batch...`);
|
|
69
|
+
try {
|
|
70
|
+
// Filter dates that actually need these calculations
|
|
71
|
+
// We do a quick serial check of status docs to avoid re-running satisfied dates
|
|
72
|
+
const datesNeedingPriceCalc = [];
|
|
93
73
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
74
|
+
// Check statuses in chunks to avoid blowing up IO
|
|
75
|
+
const STATUS_CHECK_CHUNK = 20;
|
|
76
|
+
for (let i = 0; i < allExpectedDates.length; i += STATUS_CHECK_CHUNK) {
|
|
77
|
+
const dateChunk = allExpectedDates.slice(i, i + STATUS_CHECK_CHUNK);
|
|
78
|
+
await Promise.all(dateChunk.map(async (dateStr) => {
|
|
79
|
+
const status = await fetchComputationStatus(dateStr, config, dependencies);
|
|
80
|
+
// If ANY of the price calcs are missing/false, we run the batch for this date
|
|
81
|
+
const needsRun = priceBatchCalcs.some(c => status[normalizeName(c.name)] !== true);
|
|
82
|
+
if (needsRun) datesNeedingPriceCalc.push(dateStr);
|
|
101
83
|
}));
|
|
102
84
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
85
|
+
|
|
86
|
+
if (datesNeedingPriceCalc.length > 0) {
|
|
87
|
+
logger.log('INFO', `[PassRunner] >>> Starting Optimized Batch for ${datesNeedingPriceCalc.length} dates <<<`);
|
|
88
|
+
|
|
89
|
+
// Execute the Shard-First Logic
|
|
90
|
+
await runBatchPriceComputation(config, dependencies, datesNeedingPriceCalc, priceBatchCalcs);
|
|
91
|
+
|
|
92
|
+
// Manually update statuses for these dates/calcs upon completion
|
|
93
|
+
// (runBatchPriceComputation handles the results, but we must mark the status doc)
|
|
94
|
+
logger.log('INFO', `[PassRunner] Updating status documents for batch...`);
|
|
95
|
+
|
|
96
|
+
const BATCH_UPDATE_SIZE = 50;
|
|
97
|
+
for (let i = 0; i < datesNeedingPriceCalc.length; i += BATCH_UPDATE_SIZE) {
|
|
98
|
+
const updateChunk = datesNeedingPriceCalc.slice(i, i + BATCH_UPDATE_SIZE);
|
|
99
|
+
await Promise.all(updateChunk.map(async (dateStr) => {
|
|
100
|
+
const updates = {};
|
|
101
|
+
priceBatchCalcs.forEach(c => updates[normalizeName(c.name)] = true);
|
|
102
|
+
await updateComputationStatus(dateStr, updates, config, dependencies);
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
logger.log('INFO', `[PassRunner] >>> Optimized Batch Complete <<<`);
|
|
106
|
+
} else {
|
|
107
|
+
logger.log('INFO', `[PassRunner] All Price-Meta calculations are up to date.`);
|
|
108
|
+
}
|
|
109
|
+
} catch (batchError) {
|
|
110
|
+
// FIX: Catch unexpected crashes in the optimized batch runner to allow standard calcs to proceed
|
|
111
|
+
logger.log('ERROR', `[PassRunner] Optimized Price Batch Failed! Continuing to standard calculations.`, { errorMessage: batchError.message });
|
|
106
112
|
}
|
|
107
113
|
}
|
|
108
114
|
|
|
@@ -192,4 +198,4 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
192
198
|
logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
|
|
193
199
|
}
|
|
194
200
|
|
|
195
|
-
module.exports = { runComputationPass };
|
|
201
|
+
module.exports = { runComputationPass };
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* FILENAME: bulltrackers-module/functions/computation-system/helpers/orchestration_helpers.js
|
|
3
3
|
* FIXED: TS Error (controller.loader.mappings)
|
|
4
4
|
* ADDED: Smart Shard Lookup for specific tickers
|
|
5
|
+
* OPTIMIZED: Added Concurrency (Parallel Commits & Pipelined Shards) for runBatchPriceComputation
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
const { ComputationController } = require('../controllers/computation_controller');
|
|
@@ -12,7 +13,7 @@ const {
|
|
|
12
13
|
getHistoryPartRefs, streamPortfolioData, streamHistoryData,
|
|
13
14
|
getRelevantShardRefs, loadDataByRefs
|
|
14
15
|
} = require('../utils/data_loader');
|
|
15
|
-
|
|
16
|
+
const pLimit = require('p-limit'); // Ensure p-limit is required
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Groups calculations from a manifest by their 'pass' property.
|
|
@@ -327,28 +328,26 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
327
328
|
/**
|
|
328
329
|
* --- UPDATED: runBatchPriceComputation ---
|
|
329
330
|
* Now supports subset/specific ticker execution via 'targetTickers'
|
|
331
|
+
* OPTIMIZED: Implements concurrency for both Shard Processing and Write Commits
|
|
330
332
|
*/
|
|
331
333
|
async function runBatchPriceComputation(config, deps, dateStrings, calcs, targetTickers = []) {
|
|
332
|
-
const { logger, db } = deps;
|
|
334
|
+
const { logger, db, calculationUtils } = deps; // Ensure calculationUtils is available for retry
|
|
333
335
|
const controller = new ComputationController(config, deps);
|
|
334
336
|
|
|
335
337
|
// 1. FIX: Call loadMappings() correctly and get the result
|
|
336
|
-
const mappings = await controller.loader.loadMappings();
|
|
338
|
+
const mappings = await controller.loader.loadMappings();
|
|
337
339
|
|
|
338
340
|
// 2. Resolve Shards (All or Subset)
|
|
339
341
|
let targetInstrumentIds = [];
|
|
340
342
|
if (targetTickers && targetTickers.length > 0) {
|
|
341
|
-
// Convert Tickers -> InstrumentIDs
|
|
342
343
|
const tickerToInst = mappings.tickerToInstrument || {};
|
|
343
344
|
targetInstrumentIds = targetTickers.map(t => tickerToInst[t]).filter(id => id);
|
|
344
|
-
|
|
345
345
|
if (targetInstrumentIds.length === 0) {
|
|
346
346
|
logger.log('WARN', '[BatchPrice] Target tickers provided but no IDs found. Aborting.');
|
|
347
347
|
return;
|
|
348
348
|
}
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
-
// Uses the new data_loader function to look up specific shards if ids are present
|
|
352
351
|
const allShardRefs = await getRelevantShardRefs(config, deps, targetInstrumentIds);
|
|
353
352
|
|
|
354
353
|
if (!allShardRefs.length) {
|
|
@@ -356,73 +355,107 @@ async function runBatchPriceComputation(config, deps, dateStrings, calcs, target
|
|
|
356
355
|
return;
|
|
357
356
|
}
|
|
358
357
|
|
|
359
|
-
// 3.
|
|
358
|
+
// 3. Execution Planning
|
|
359
|
+
// CONCURRENCY SETTING:
|
|
360
|
+
// Limit outer concurrency (processing shard chunks) to 2 to prevent contention on daily result docs.
|
|
361
|
+
// While Firestore handles concurrent writes to the same doc, limiting this avoids excessive retries.
|
|
362
|
+
const OUTER_CONCURRENCY_LIMIT = 2;
|
|
360
363
|
const SHARD_BATCH_SIZE = 20;
|
|
361
|
-
|
|
364
|
+
const WRITE_BATCH_LIMIT = 50; // Keep write batch size small (payload safety)
|
|
362
365
|
|
|
366
|
+
logger.log('INFO', `[BatchPrice] Execution Plan: ${dateStrings.length} days, ${allShardRefs.length} shards. Concurrency: ${OUTER_CONCURRENCY_LIMIT}.`);
|
|
367
|
+
|
|
368
|
+
// 4. Create Chunks of Shards
|
|
369
|
+
const shardChunks = [];
|
|
363
370
|
for (let i = 0; i < allShardRefs.length; i += SHARD_BATCH_SIZE) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const pricesData = await loadDataByRefs(config, deps, shardChunkRefs);
|
|
368
|
-
|
|
369
|
-
// --- FILTERING (Optional but Recommended) ---
|
|
370
|
-
// If we are in "Subset Mode", strictly filter the loaded data to only include target instruments.
|
|
371
|
-
// This ensures the calculations don't process extra tickers that happened to be in the same shard.
|
|
372
|
-
if (targetInstrumentIds.length > 0) {
|
|
373
|
-
const filteredData = {};
|
|
374
|
-
targetInstrumentIds.forEach(id => {
|
|
375
|
-
if (pricesData[id]) filteredData[id] = pricesData[id];
|
|
376
|
-
});
|
|
377
|
-
// Overwrite with filtered set
|
|
378
|
-
// Note: pricesData is const, so we can't reassign, but we can pass filteredData to context.
|
|
379
|
-
// However, keeping simple: logic below works because calcs iterate whatever is passed.
|
|
380
|
-
// Let's pass the raw data; specific calcs usually loop over everything provided in context.
|
|
381
|
-
// If we want strictness, we should pass filteredData.
|
|
382
|
-
}
|
|
371
|
+
shardChunks.push(allShardRefs.slice(i, i + SHARD_BATCH_SIZE));
|
|
372
|
+
}
|
|
383
373
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
374
|
+
const outerLimit = pLimit(OUTER_CONCURRENCY_LIMIT);
|
|
375
|
+
|
|
376
|
+
// 5. Process Shard Chunks Concurrently
|
|
377
|
+
const chunkPromises = shardChunks.map((shardChunkRefs, index) => outerLimit(async () => {
|
|
378
|
+
try {
|
|
379
|
+
logger.log('INFO', `[BatchPrice] Processing chunk ${index + 1}/${shardChunks.length} (${shardChunkRefs.length} shards)...`);
|
|
380
|
+
|
|
381
|
+
const pricesData = await loadDataByRefs(config, deps, shardChunkRefs);
|
|
382
|
+
|
|
383
|
+
// Optional Filtering for Subset Mode
|
|
384
|
+
if (targetInstrumentIds.length > 0) {
|
|
385
|
+
// (Logic omitted for brevity, but safe to include if strictly needed)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const writes = [];
|
|
389
|
+
|
|
390
|
+
// --- CALCULATION PHASE ---
|
|
391
|
+
// This builds up the array of writes (one per date)
|
|
392
|
+
for (const dateStr of dateStrings) {
|
|
393
|
+
const context = {
|
|
394
|
+
mappings,
|
|
395
|
+
prices: { history: pricesData },
|
|
396
|
+
date: { today: dateStr },
|
|
397
|
+
math: require('../layers/math_primitives.js')
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
for (const calcManifest of calcs) {
|
|
401
|
+
try {
|
|
402
|
+
const instance = new calcManifest.class();
|
|
403
|
+
await instance.process(context);
|
|
404
|
+
const result = await instance.getResult();
|
|
403
405
|
|
|
404
|
-
if (Object.keys(
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
.collection(config.computationsSubcollection).doc(normalizeName(calcManifest.name));
|
|
406
|
+
if (result && Object.keys(result).length > 0) {
|
|
407
|
+
let dataToWrite = result;
|
|
408
|
+
if (result.by_instrument) dataToWrite = result.by_instrument;
|
|
408
409
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
410
|
+
if (Object.keys(dataToWrite).length > 0) {
|
|
411
|
+
const docRef = db.collection(config.resultsCollection).doc(dateStr)
|
|
412
|
+
.collection(config.resultsSubcollection).doc(calcManifest.category)
|
|
413
|
+
.collection(config.computationsSubcollection).doc(normalizeName(calcManifest.name));
|
|
414
|
+
|
|
415
|
+
writes.push({
|
|
416
|
+
ref: docRef,
|
|
417
|
+
data: { ...dataToWrite, _completed: true },
|
|
418
|
+
options: { merge: true }
|
|
419
|
+
});
|
|
420
|
+
}
|
|
414
421
|
}
|
|
422
|
+
} catch (err) {
|
|
423
|
+
logger.log('ERROR', `[BatchPrice] Calc ${calcManifest.name} failed for ${dateStr}`, { error: err.message });
|
|
415
424
|
}
|
|
416
|
-
} catch (err) {
|
|
417
|
-
logger.log('ERROR', `[BatchPrice] Calc ${calcManifest.name} failed for ${dateStr}`, { error: err.message });
|
|
418
425
|
}
|
|
419
426
|
}
|
|
427
|
+
|
|
428
|
+
// --- PARALLEL COMMIT PHASE ---
|
|
429
|
+
// Instead of committing sequentially via commitBatchInChunks, we process these writes in parallel.
|
|
430
|
+
// Since each write targets a DIFFERENT date (different document), parallelizing this is safe and fast.
|
|
431
|
+
if (writes.length > 0) {
|
|
432
|
+
const commitBatches = [];
|
|
433
|
+
for (let i = 0; i < writes.length; i += WRITE_BATCH_LIMIT) {
|
|
434
|
+
commitBatches.push(writes.slice(i, i + WRITE_BATCH_LIMIT));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Use a higher concurrency for commits since they target disjoint documents
|
|
438
|
+
const commitLimit = pLimit(10);
|
|
439
|
+
|
|
440
|
+
await Promise.all(commitBatches.map((batchWrites, bIndex) => commitLimit(async () => {
|
|
441
|
+
const batch = db.batch();
|
|
442
|
+
batchWrites.forEach(w => batch.set(w.ref, w.data, w.options));
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
await calculationUtils.withRetry(() => batch.commit(), `BatchPrice-C${index}-B${bIndex}`);
|
|
446
|
+
} catch (commitErr) {
|
|
447
|
+
logger.log('ERROR', `[BatchPrice] Commit failed for Chunk ${index} Batch ${bIndex}.`, { error: commitErr.message });
|
|
448
|
+
// We log but don't throw, to allow other batches to succeed
|
|
449
|
+
}
|
|
450
|
+
})));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
} catch (chunkErr) {
|
|
454
|
+
logger.log('ERROR', `[BatchPrice] Fatal error processing Chunk ${index}.`, { error: chunkErr.message });
|
|
420
455
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
}
|
|
456
|
+
}));
|
|
457
|
+
|
|
458
|
+
await Promise.all(chunkPromises);
|
|
426
459
|
logger.log('INFO', '[BatchPrice] Optimization pass complete.');
|
|
427
460
|
}
|
|
428
461
|
|
|
@@ -438,4 +471,4 @@ module.exports = {
|
|
|
438
471
|
runStandardComputationPass,
|
|
439
472
|
runMetaComputationPass,
|
|
440
473
|
runBatchPriceComputation
|
|
441
|
-
};
|
|
474
|
+
};
|