bulltrackers-module 1.0.219 → 1.0.220
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/functions/computation-system/WorkflowOrchestrator.js +153 -0
- package/functions/computation-system/context/ContextFactory.js +63 -0
- package/functions/computation-system/context/ManifestBuilder.js +240 -0
- package/functions/computation-system/controllers/computation_controller.js +12 -4
- package/functions/computation-system/data/AvailabilityChecker.js +75 -0
- package/functions/computation-system/data/CachedDataLoader.js +63 -0
- package/functions/computation-system/data/DependencyFetcher.js +70 -0
- package/functions/computation-system/executors/MetaExecutor.js +68 -0
- package/functions/computation-system/executors/PriceBatchExecutor.js +99 -0
- package/functions/computation-system/executors/StandardExecutor.js +115 -0
- package/functions/computation-system/helpers/computation_dispatcher.js +3 -3
- package/functions/computation-system/helpers/computation_worker.js +44 -18
- package/functions/computation-system/layers/mathematics.js +1 -1
- package/functions/computation-system/persistence/FirestoreUtils.js +64 -0
- package/functions/computation-system/persistence/ResultCommitter.js +118 -0
- package/functions/computation-system/persistence/StatusRepository.js +23 -0
- package/functions/computation-system/topology/HashManager.js +35 -0
- package/functions/computation-system/utils/utils.js +38 -10
- package/index.js +8 -3
- package/package.json +1 -1
- package/functions/computation-system/helpers/computation_manifest_builder.js +0 -320
- package/functions/computation-system/helpers/computation_pass_runner.js +0 -119
- package/functions/computation-system/helpers/orchestration_helpers.js +0 -352
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Fetches results from previous computations, handling auto-sharding hydration.
|
|
3
|
+
*/
|
|
4
|
+
const { normalizeName } = require('../utils/utils');
|
|
5
|
+
|
|
6
|
+
async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config, { db }, includeSelf = false) {
|
|
7
|
+
const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
|
|
8
|
+
const calcsToFetch = new Set();
|
|
9
|
+
|
|
10
|
+
for (const calc of calcsInPass) {
|
|
11
|
+
if (calc.dependencies) calc.dependencies.forEach(d => calcsToFetch.add(normalizeName(d)));
|
|
12
|
+
if (includeSelf && calc.isHistorical) calcsToFetch.add(normalizeName(calc.name));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!calcsToFetch.size) return {};
|
|
16
|
+
|
|
17
|
+
const fetched = {};
|
|
18
|
+
const docRefs = [];
|
|
19
|
+
const names = [];
|
|
20
|
+
|
|
21
|
+
for (const name of calcsToFetch) {
|
|
22
|
+
const m = manifestMap.get(name);
|
|
23
|
+
if (m) {
|
|
24
|
+
docRefs.push(db.collection(config.resultsCollection)
|
|
25
|
+
.doc(dateStr)
|
|
26
|
+
.collection(config.resultsSubcollection)
|
|
27
|
+
.doc(m.category || 'unknown')
|
|
28
|
+
.collection(config.computationsSubcollection)
|
|
29
|
+
.doc(name));
|
|
30
|
+
names.push(name);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (docRefs.length) {
|
|
35
|
+
const snaps = await db.getAll(...docRefs);
|
|
36
|
+
const hydrationPromises = [];
|
|
37
|
+
|
|
38
|
+
snaps.forEach((doc, i) => {
|
|
39
|
+
const name = names[i];
|
|
40
|
+
if (!doc.exists) return;
|
|
41
|
+
const data = doc.data();
|
|
42
|
+
if (data._sharded === true) {
|
|
43
|
+
hydrationPromises.push(hydrateAutoShardedResult(doc.ref, name));
|
|
44
|
+
} else if (data._completed) {
|
|
45
|
+
fetched[name] = data;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (hydrationPromises.length > 0) {
|
|
50
|
+
const hydratedResults = await Promise.all(hydrationPromises);
|
|
51
|
+
hydratedResults.forEach(res => { fetched[res.name] = res.data; });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return fetched;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function hydrateAutoShardedResult(docRef, resultName) {
|
|
58
|
+
const shardsCol = docRef.collection('_shards');
|
|
59
|
+
const snapshot = await shardsCol.get();
|
|
60
|
+
const assembledData = { _completed: true };
|
|
61
|
+
snapshot.forEach(doc => {
|
|
62
|
+
const chunk = doc.data();
|
|
63
|
+
Object.assign(assembledData, chunk);
|
|
64
|
+
});
|
|
65
|
+
delete assembledData._sharded;
|
|
66
|
+
delete assembledData._completed;
|
|
67
|
+
return { name: resultName, data: assembledData };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { fetchExistingResults };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Executor for "Meta" (global) calculations.
|
|
3
|
+
*/
|
|
4
|
+
const { normalizeName } = require('../utils/utils');
|
|
5
|
+
const { CachedDataLoader } = require('../data/CachedDataLoader');
|
|
6
|
+
const { ContextFactory } = require('../context/ContextFactory');
|
|
7
|
+
const { commitResults } = require('../persistence/ResultCommitter');
|
|
8
|
+
|
|
9
|
+
class MetaExecutor {
|
|
10
|
+
static async run(date, calcs, passName, config, deps, fetchedDeps, previousFetchedDeps, rootData, skipStatusWrite = false) {
|
|
11
|
+
const dStr = date.toISOString().slice(0, 10);
|
|
12
|
+
const state = {};
|
|
13
|
+
const cachedLoader = new CachedDataLoader(config, deps);
|
|
14
|
+
|
|
15
|
+
for (const mCalc of calcs) {
|
|
16
|
+
try {
|
|
17
|
+
deps.logger.log('INFO', `${mCalc.name} calculation running for ${dStr}`);
|
|
18
|
+
const inst = new mCalc.class();
|
|
19
|
+
inst.manifest = mCalc;
|
|
20
|
+
|
|
21
|
+
await MetaExecutor.executeOncePerDay(inst, mCalc, dStr, fetchedDeps, previousFetchedDeps, config, deps, cachedLoader);
|
|
22
|
+
state[normalizeName(mCalc.name)] = inst;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
deps.logger.log('ERROR', `Meta calc failed ${mCalc.name}: ${e.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return await commitResults(state, dStr, passName, config, deps, skipStatusWrite);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static async executeOncePerDay(calcInstance, metadata, dateStr, computedDeps, prevDeps, config, deps, loader) {
|
|
31
|
+
const mappings = await loader.loadMappings();
|
|
32
|
+
const { logger } = deps;
|
|
33
|
+
const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await loader.loadInsights(dateStr) } : null;
|
|
34
|
+
const social = metadata.rootDataDependencies?.includes('social') ? { today: await loader.loadSocial(dateStr) } : null;
|
|
35
|
+
|
|
36
|
+
if (metadata.rootDataDependencies?.includes('price')) {
|
|
37
|
+
logger.log('INFO', `[Executor] Running Batched/Sharded Execution for ${metadata.name}`);
|
|
38
|
+
const shardRefs = await loader.getPriceShardReferences();
|
|
39
|
+
if (shardRefs.length === 0) { logger.log('WARN', '[Executor] No price shards found.'); return {}; }
|
|
40
|
+
|
|
41
|
+
let processedCount = 0;
|
|
42
|
+
for (const ref of shardRefs) {
|
|
43
|
+
const shardData = await loader.loadPriceShard(ref);
|
|
44
|
+
const partialContext = ContextFactory.buildMetaContext({
|
|
45
|
+
dateStr, metadata, mappings, insights, socialData: social,
|
|
46
|
+
prices: { history: shardData }, computedDependencies: computedDeps,
|
|
47
|
+
previousComputedDependencies: prevDeps, config, deps
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await calcInstance.process(partialContext);
|
|
51
|
+
partialContext.prices = null;
|
|
52
|
+
processedCount++;
|
|
53
|
+
if (processedCount % 10 === 0 && global.gc) { global.gc(); }
|
|
54
|
+
}
|
|
55
|
+
logger.log('INFO', `[Executor] Finished Batched Execution for ${metadata.name} (${processedCount} shards).`);
|
|
56
|
+
return calcInstance.getResult ? await calcInstance.getResult() : {};
|
|
57
|
+
} else {
|
|
58
|
+
const context = ContextFactory.buildMetaContext({
|
|
59
|
+
dateStr, metadata, mappings, insights, socialData: social,
|
|
60
|
+
prices: {}, computedDependencies: computedDeps,
|
|
61
|
+
previousComputedDependencies: prevDeps, config, deps
|
|
62
|
+
});
|
|
63
|
+
return await calcInstance.process(context);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { MetaExecutor };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Specialized Executor for Price-Dependent Batch computations.
|
|
3
|
+
*/
|
|
4
|
+
const pLimit = require('p-limit');
|
|
5
|
+
const { normalizeName } = require('../utils/utils');
|
|
6
|
+
const { getRelevantShardRefs, loadDataByRefs } = require('../utils/data_loader');
|
|
7
|
+
const { CachedDataLoader } = require('../data/CachedDataLoader');
|
|
8
|
+
const mathLayer = require('../layers/index');
|
|
9
|
+
const { LEGACY_MAPPING } = require('../topology/HashManager');
|
|
10
|
+
|
|
11
|
+
async function runBatchPriceComputation(config, deps, dateStrings, calcs, targetTickers = []) {
|
|
12
|
+
const { logger, db, calculationUtils } = deps;
|
|
13
|
+
const cachedLoader = new CachedDataLoader(config, deps);
|
|
14
|
+
const mappings = await cachedLoader.loadMappings();
|
|
15
|
+
|
|
16
|
+
let targetInstrumentIds = [];
|
|
17
|
+
if (targetTickers && targetTickers.length > 0) {
|
|
18
|
+
const tickerToInst = mappings.tickerToInstrument || {};
|
|
19
|
+
targetInstrumentIds = targetTickers.map(t => tickerToInst[t]).filter(id => id);
|
|
20
|
+
if (targetInstrumentIds.length === 0) { logger.log('WARN', '[BatchPrice] Target tickers provided but no IDs found. Aborting.'); return; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const allShardRefs = await getRelevantShardRefs(config, deps, targetInstrumentIds);
|
|
24
|
+
if (!allShardRefs.length) { logger.log('WARN', '[BatchPrice] No relevant price shards found. Exiting.'); return; }
|
|
25
|
+
|
|
26
|
+
const OUTER_CONCURRENCY_LIMIT = 2, SHARD_BATCH_SIZE = 20, WRITE_BATCH_LIMIT = 50;
|
|
27
|
+
logger.log('INFO', `[BatchPrice] Execution Plan: ${dateStrings.length} days, ${allShardRefs.length} shards. Concurrency: ${OUTER_CONCURRENCY_LIMIT}.`);
|
|
28
|
+
|
|
29
|
+
const shardChunks = [];
|
|
30
|
+
for (let i = 0; i < allShardRefs.length; i += SHARD_BATCH_SIZE) { shardChunks.push(allShardRefs.slice(i, i + SHARD_BATCH_SIZE)); }
|
|
31
|
+
|
|
32
|
+
const outerLimit = pLimit(OUTER_CONCURRENCY_LIMIT);
|
|
33
|
+
const chunkPromises = [];
|
|
34
|
+
|
|
35
|
+
for (let index = 0; index < shardChunks.length; index++) {
|
|
36
|
+
const shardChunkRefs = shardChunks[index];
|
|
37
|
+
chunkPromises.push(outerLimit(async () => {
|
|
38
|
+
try {
|
|
39
|
+
logger.log('INFO', `[BatchPrice] Processing chunk ${index + 1}/${shardChunks.length} (${shardChunkRefs.length} shards)...`);
|
|
40
|
+
const pricesData = await loadDataByRefs(config, deps, shardChunkRefs);
|
|
41
|
+
if (targetInstrumentIds.length > 0) {
|
|
42
|
+
const requestedSet = new Set(targetInstrumentIds);
|
|
43
|
+
for (const loadedInstrumentId in pricesData) {
|
|
44
|
+
if (!requestedSet.has(loadedInstrumentId)) { delete pricesData[loadedInstrumentId]; }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const writes = [];
|
|
49
|
+
for (const dateStr of dateStrings) {
|
|
50
|
+
const dynamicMathContext = {};
|
|
51
|
+
for (const [key, value] of Object.entries(mathLayer)) {
|
|
52
|
+
dynamicMathContext[key] = value;
|
|
53
|
+
if (LEGACY_MAPPING[key]) { dynamicMathContext[LEGACY_MAPPING[key]] = value;}
|
|
54
|
+
}
|
|
55
|
+
const context = { mappings, prices: { history: pricesData }, date: { today: dateStr }, math: dynamicMathContext };
|
|
56
|
+
|
|
57
|
+
for (const calcManifest of calcs) {
|
|
58
|
+
try {
|
|
59
|
+
const instance = new calcManifest.class();
|
|
60
|
+
await instance.process(context);
|
|
61
|
+
const result = await instance.getResult();
|
|
62
|
+
if (result && Object.keys(result).length > 0) {
|
|
63
|
+
let dataToWrite = result;
|
|
64
|
+
if (result.by_instrument) dataToWrite = result.by_instrument;
|
|
65
|
+
if (Object.keys(dataToWrite).length > 0) {
|
|
66
|
+
const docRef = db.collection(config.resultsCollection)
|
|
67
|
+
.doc(dateStr)
|
|
68
|
+
.collection(config.resultsSubcollection)
|
|
69
|
+
.doc(calcManifest.category)
|
|
70
|
+
.collection(config.computationsSubcollection)
|
|
71
|
+
.doc(normalizeName(calcManifest.name));
|
|
72
|
+
writes.push({ ref: docRef, data: { ...dataToWrite, _completed: true }, options: { merge: true } });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch (err) { logger.log('ERROR', `[BatchPrice] \u2716 Failed ${calcManifest.name} for ${dateStr}: ${err.message}`); }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (writes.length > 0) {
|
|
80
|
+
const commitBatches = [];
|
|
81
|
+
for (let i = 0; i < writes.length; i += WRITE_BATCH_LIMIT) { commitBatches.push(writes.slice(i, i + WRITE_BATCH_LIMIT)); }
|
|
82
|
+
const commitLimit = pLimit(10);
|
|
83
|
+
await Promise.all(commitBatches.map((batchWrites, bIndex) => commitLimit(async () => {
|
|
84
|
+
const batch = db.batch(); batchWrites.forEach(w => batch.set(w.ref, w.data, w.options));
|
|
85
|
+
try {
|
|
86
|
+
await calculationUtils.withRetry(() => batch.commit(), `BatchPrice-C${index}-B${bIndex}`);
|
|
87
|
+
} catch (commitErr) {
|
|
88
|
+
logger.log('ERROR', `[BatchPrice] Commit failed for Chunk ${index} Batch ${bIndex}.`, { error: commitErr.message });
|
|
89
|
+
}
|
|
90
|
+
})));
|
|
91
|
+
}
|
|
92
|
+
} catch (chunkErr) { logger.log('ERROR', `[BatchPrice] Fatal error processing Chunk ${index}.`, { error: chunkErr.message }); }
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
await Promise.all(chunkPromises);
|
|
96
|
+
logger.log('INFO', '[BatchPrice] Optimization pass complete.');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { runBatchPriceComputation };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Executor for "Standard" (per-user) calculations.
|
|
3
|
+
*/
|
|
4
|
+
const { normalizeName } = require('../utils/utils');
|
|
5
|
+
const { streamPortfolioData, streamHistoryData, getPortfolioPartRefs } = require('../utils/data_loader');
|
|
6
|
+
const { CachedDataLoader } = require('../data/CachedDataLoader');
|
|
7
|
+
const { ContextFactory } = require('../context/ContextFactory');
|
|
8
|
+
const { commitResults } = require('../persistence/ResultCommitter');
|
|
9
|
+
const mathLayer = require('../layers/index');
|
|
10
|
+
|
|
11
|
+
class StandardExecutor {
|
|
12
|
+
static async run(date, calcs, passName, config, deps, rootData, fetchedDeps, previousFetchedDeps, skipStatusWrite = false) {
|
|
13
|
+
const dStr = date.toISOString().slice(0, 10);
|
|
14
|
+
const logger = deps.logger;
|
|
15
|
+
|
|
16
|
+
// 1. Prepare Yesterdays Data if needed
|
|
17
|
+
const fullRoot = { ...rootData };
|
|
18
|
+
if (calcs.some(c => c.isHistorical)) {
|
|
19
|
+
const prev = new Date(date); prev.setUTCDate(prev.getUTCDate() - 1);
|
|
20
|
+
const prevStr = prev.toISOString().slice(0, 10);
|
|
21
|
+
fullRoot.yesterdayPortfolioRefs = await getPortfolioPartRefs(config, deps, prevStr);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 2. Initialize Instances
|
|
25
|
+
const state = {};
|
|
26
|
+
for (const c of calcs) {
|
|
27
|
+
try {
|
|
28
|
+
const inst = new c.class();
|
|
29
|
+
inst.manifest = c;
|
|
30
|
+
state[normalizeName(c.name)] = inst;
|
|
31
|
+
logger.log('INFO', `${c.name} calculation running for ${dStr}`);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
logger.log('WARN', `Failed to init ${c.name}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 3. Stream & Process
|
|
38
|
+
await StandardExecutor.streamAndProcess(dStr, state, passName, config, deps, fullRoot, rootData.portfolioRefs, rootData.historyRefs, fetchedDeps, previousFetchedDeps);
|
|
39
|
+
|
|
40
|
+
// 4. Commit
|
|
41
|
+
return await commitResults(state, dStr, passName, config, deps, skipStatusWrite);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static async streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs, fetchedDeps, previousFetchedDeps) {
|
|
45
|
+
const { logger } = deps;
|
|
46
|
+
const calcs = Object.values(state).filter(c => c && c.manifest);
|
|
47
|
+
const streamingCalcs = calcs.filter(c => c.manifest.rootDataDependencies.includes('portfolio') || c.manifest.rootDataDependencies.includes('history'));
|
|
48
|
+
|
|
49
|
+
if (streamingCalcs.length === 0) return;
|
|
50
|
+
|
|
51
|
+
logger.log('INFO', `[${passName}] Streaming for ${streamingCalcs.length} computations...`);
|
|
52
|
+
|
|
53
|
+
const cachedLoader = new CachedDataLoader(config, deps);
|
|
54
|
+
await cachedLoader.loadMappings();
|
|
55
|
+
|
|
56
|
+
const prevDate = new Date(dateStr + 'T00:00:00Z'); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
57
|
+
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
58
|
+
|
|
59
|
+
const tP_iter = streamPortfolioData(config, deps, dateStr, portfolioRefs);
|
|
60
|
+
const needsYesterdayPortfolio = streamingCalcs.some(c => c.manifest.isHistorical);
|
|
61
|
+
const yP_iter = (needsYesterdayPortfolio && rootData.yesterdayPortfolioRefs)
|
|
62
|
+
? streamPortfolioData(config, deps, prevDateStr, rootData.yesterdayPortfolioRefs)
|
|
63
|
+
: null;
|
|
64
|
+
|
|
65
|
+
const needsTradingHistory = streamingCalcs.some(c => c.manifest.rootDataDependencies.includes('history'));
|
|
66
|
+
const tH_iter = (needsTradingHistory && historyRefs)
|
|
67
|
+
? streamHistoryData(config, deps, dateStr, historyRefs)
|
|
68
|
+
: null;
|
|
69
|
+
|
|
70
|
+
let yP_chunk = {}, tH_chunk = {};
|
|
71
|
+
|
|
72
|
+
for await (const tP_chunk of tP_iter) {
|
|
73
|
+
if (yP_iter) yP_chunk = (await yP_iter.next()).value || {};
|
|
74
|
+
if (tH_iter) tH_chunk = (await tH_iter.next()).value || {};
|
|
75
|
+
|
|
76
|
+
// Execute chunk for all calcs
|
|
77
|
+
const promises = streamingCalcs.map(calc =>
|
|
78
|
+
StandardExecutor.executePerUser(calc, calc.manifest, dateStr, tP_chunk, yP_chunk, tH_chunk, fetchedDeps, previousFetchedDeps, config, deps, cachedLoader)
|
|
79
|
+
);
|
|
80
|
+
await Promise.all(promises);
|
|
81
|
+
}
|
|
82
|
+
logger.log('INFO', `[${passName}] Streaming complete.`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps, config, deps, loader) {
|
|
86
|
+
const { logger } = deps;
|
|
87
|
+
const targetUserType = metadata.userType;
|
|
88
|
+
const mappings = await loader.loadMappings();
|
|
89
|
+
const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await loader.loadInsights(dateStr) } : null;
|
|
90
|
+
const SCHEMAS = mathLayer.SCHEMAS;
|
|
91
|
+
|
|
92
|
+
for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
|
|
93
|
+
const yesterdayPortfolio = yesterdayPortfolioData ? yesterdayPortfolioData[userId] : null;
|
|
94
|
+
const todayHistory = historyData ? historyData[userId] : null;
|
|
95
|
+
const actualUserType = todayPortfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
|
|
96
|
+
|
|
97
|
+
if (targetUserType !== 'all') {
|
|
98
|
+
const mappedTarget = (targetUserType === 'speculator') ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
|
|
99
|
+
if (mappedTarget !== actualUserType) continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const context = ContextFactory.buildPerUserContext({
|
|
103
|
+
todayPortfolio, yesterdayPortfolio, todayHistory, userId,
|
|
104
|
+
userType: actualUserType, dateStr, metadata, mappings, insights,
|
|
105
|
+
computedDependencies: computedDeps, previousComputedDependencies: prevDeps,
|
|
106
|
+
config, deps
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
try { await calcInstance.process(context); }
|
|
110
|
+
catch (e) { logger.log('WARN', `Calc ${metadata.name} failed for user ${userId}: ${e.message}`); }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { StandardExecutor };
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FILENAME: bulltrackers-module/functions/computation-system/helpers/computation_dispatcher.js
|
|
3
3
|
* PURPOSE: Dispatches computation tasks to Pub/Sub for scalable execution.
|
|
4
|
-
*
|
|
5
|
-
* IMPROVED: Logging now explicitly lists the calculations being scheduled.
|
|
4
|
+
* REFACTORED: Now uses WorkflowOrchestrator for helper functions.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
const { getExpectedDateStrings } = require('../utils/utils.js');
|
|
9
|
-
const { groupByPass } = require('
|
|
8
|
+
const { groupByPass } = require('../WorkflowOrchestrator.js');
|
|
10
9
|
const { PubSubUtils } = require('../../core/utils/pubsub_utils');
|
|
11
10
|
|
|
12
11
|
const TOPIC_NAME = 'computation-tasks';
|
|
@@ -19,6 +18,7 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
19
18
|
const { logger } = dependencies;
|
|
20
19
|
const pubsubUtils = new PubSubUtils(dependencies);
|
|
21
20
|
const passToRun = String(config.COMPUTATION_PASS_TO_RUN);
|
|
21
|
+
|
|
22
22
|
if (!passToRun) { return logger.log('ERROR', '[Dispatcher] No pass defined (COMPUTATION_PASS_TO_RUN). Aborting.'); }
|
|
23
23
|
|
|
24
24
|
// 1. Validate Pass Existence
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FILENAME: bulltrackers-module/functions/computation-system/helpers/computation_worker.js
|
|
3
3
|
* PURPOSE: Consumes computation tasks from Pub/Sub and executes them.
|
|
4
|
-
*
|
|
4
|
+
* REFACTORED: Now imports logic from the new WorkflowOrchestrator.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const { runDateComputation } = require('
|
|
8
|
-
const { groupByPass } = require('./orchestration_helpers.js');
|
|
7
|
+
const { runDateComputation, groupByPass } = require('../WorkflowOrchestrator.js');
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* Handles a single Pub/Sub message for a computation task.
|
|
@@ -16,29 +15,56 @@ async function handleComputationTask(message, config, dependencies, computationM
|
|
|
16
15
|
|
|
17
16
|
let data;
|
|
18
17
|
try {
|
|
19
|
-
// 1. Handle Cloud Functions Gen 2 (CloudEvent)
|
|
20
|
-
if (message.data && message.data.message && message.data.message.data) {
|
|
21
|
-
|
|
22
|
-
else if (message.data && typeof message.data === 'string') {
|
|
23
|
-
|
|
24
|
-
else if (message.json) {
|
|
25
|
-
|
|
26
|
-
else {
|
|
27
|
-
|
|
18
|
+
// 1. Handle Cloud Functions Gen 2 (CloudEvent) -> Gen 1 -> Direct JSON -> Message
|
|
19
|
+
if (message.data && message.data.message && message.data.message.data) {
|
|
20
|
+
const buffer = Buffer.from(message.data.message.data, 'base64'); data = JSON.parse(buffer.toString());
|
|
21
|
+
} else if (message.data && typeof message.data === 'string') {
|
|
22
|
+
const buffer = Buffer.from(message.data, 'base64'); data = JSON.parse(buffer.toString());
|
|
23
|
+
} else if (message.json) {
|
|
24
|
+
data = message.json;
|
|
25
|
+
} else {
|
|
26
|
+
data = message;
|
|
27
|
+
}
|
|
28
|
+
} catch (parseError) {
|
|
29
|
+
logger.log('ERROR', `[Worker] Failed to parse Pub/Sub payload.`, { error: parseError.message });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
28
32
|
|
|
29
33
|
try {
|
|
30
34
|
// Validate Action
|
|
31
|
-
if (!data || data.action !== 'RUN_COMPUTATION_DATE') {
|
|
35
|
+
if (!data || data.action !== 'RUN_COMPUTATION_DATE') {
|
|
36
|
+
if (data) logger.log('WARN', `[Worker] Unknown or missing action: ${data?.action}. Ignoring.`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
const { date, pass } = data;
|
|
33
|
-
if (!date || !pass) {
|
|
41
|
+
if (!date || !pass) {
|
|
42
|
+
logger.log('ERROR', `[Worker] Missing date or pass in payload: ${JSON.stringify(data)}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
logger.log('INFO', `[Worker] Received task: Date=${date}, Pass=${pass}`);
|
|
47
|
+
|
|
35
48
|
const passes = groupByPass(computationManifest);
|
|
36
49
|
const calcsInThisPass = passes[pass] || [];
|
|
37
|
-
|
|
50
|
+
|
|
51
|
+
if (!calcsInThisPass.length) {
|
|
52
|
+
logger.log('WARN', `[Worker] No calculations found for Pass ${pass}.`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
38
56
|
const result = await runDateComputation(date, pass, calcsInThisPass, config, dependencies, computationManifest);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
|
|
58
|
+
if (result) {
|
|
59
|
+
logger.log('INFO', `[Worker] Successfully processed ${date} (Pass ${pass}). Updates: ${Object.keys(result.updates || {}).length}`);
|
|
60
|
+
} else {
|
|
61
|
+
logger.log('INFO', `[Worker] Processed ${date} (Pass ${pass}) - Skipped (Dependencies missing or already done).`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
} catch (err) {
|
|
65
|
+
logger.log('ERROR', `[Worker] Fatal error processing task: ${err.message}`, { stack: err.stack });
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
42
68
|
}
|
|
43
69
|
|
|
44
70
|
module.exports = { handleComputationTask };
|
|
@@ -59,7 +59,7 @@ class MathPrimitives {
|
|
|
59
59
|
return 1.0;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
const probability = normCDF(
|
|
62
|
+
const probability = normCDF(-term3) + Math.exp(term2) * normCDF(-term1);
|
|
63
63
|
|
|
64
64
|
return Math.min(Math.max(probability, 0), 1);
|
|
65
65
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Low-level Firestore interactions.
|
|
3
|
+
*/
|
|
4
|
+
const { withRetry } = require('../utils/utils.js'); // Assuming this exists or is passed in deps
|
|
5
|
+
|
|
6
|
+
async function commitBatchInChunks(config, deps, writes, operationName) {
|
|
7
|
+
const { db, logger, calculationUtils } = deps;
|
|
8
|
+
const retryFn = calculationUtils ? calculationUtils.withRetry : (fn) => fn();
|
|
9
|
+
|
|
10
|
+
if (!writes || !writes.length) {
|
|
11
|
+
logger.log('WARN', `[${operationName}] No writes to commit.`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const MAX_BATCH_OPS = 300;
|
|
16
|
+
const MAX_BATCH_BYTES = 9 * 1024 * 1024;
|
|
17
|
+
|
|
18
|
+
let currentBatch = db.batch();
|
|
19
|
+
let currentOpsCount = 0;
|
|
20
|
+
let currentBytesEst = 0;
|
|
21
|
+
let batchIndex = 1;
|
|
22
|
+
|
|
23
|
+
const commitAndReset = async () => {
|
|
24
|
+
if (currentOpsCount > 0) {
|
|
25
|
+
try {
|
|
26
|
+
await retryFn(
|
|
27
|
+
() => currentBatch.commit(),
|
|
28
|
+
`${operationName} (Chunk ${batchIndex})`
|
|
29
|
+
);
|
|
30
|
+
logger.log('INFO', `[${operationName}] Committed chunk ${batchIndex} (${currentOpsCount} ops, ~${(currentBytesEst / 1024 / 1024).toFixed(2)} MB).`);
|
|
31
|
+
batchIndex++;
|
|
32
|
+
} catch (err) {
|
|
33
|
+
logger.log('ERROR', `[${operationName}] Failed to commit chunk ${batchIndex}. Size: ${(currentBytesEst / 1024 / 1024).toFixed(2)} MB.`, { error: err.message });
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
currentBatch = db.batch();
|
|
38
|
+
currentOpsCount = 0;
|
|
39
|
+
currentBytesEst = 0;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
for (const write of writes) {
|
|
43
|
+
let docSize = 100;
|
|
44
|
+
try { if (write.data) docSize = JSON.stringify(write.data).length; } catch (e) { }
|
|
45
|
+
|
|
46
|
+
if (docSize > 900 * 1024) {
|
|
47
|
+
logger.log('WARN', `[${operationName}] Large document detected (~${(docSize / 1024).toFixed(2)} KB).`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if ((currentOpsCount + 1 > MAX_BATCH_OPS) || (currentBytesEst + docSize > MAX_BATCH_BYTES)) {
|
|
51
|
+
await commitAndReset();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const options = write.options || { merge: true };
|
|
55
|
+
currentBatch.set(write.ref, write.data, options);
|
|
56
|
+
|
|
57
|
+
currentOpsCount++;
|
|
58
|
+
currentBytesEst += docSize;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await commitAndReset();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { commitBatchInChunks };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Handles saving computation results with transparent auto-sharding.
|
|
3
|
+
*/
|
|
4
|
+
const { commitBatchInChunks } = require('./FirestoreUtils');
|
|
5
|
+
const { updateComputationStatus } = require('./StatusRepository');
|
|
6
|
+
const { batchStoreSchemas } = require('../utils/schema_capture');
|
|
7
|
+
|
|
8
|
+
async function commitResults(stateObj, dStr, passName, config, deps, skipStatusWrite = false) {
|
|
9
|
+
const successUpdates = {};
|
|
10
|
+
const schemas = [];
|
|
11
|
+
|
|
12
|
+
for (const name in stateObj) {
|
|
13
|
+
const calc = stateObj[name];
|
|
14
|
+
try {
|
|
15
|
+
const result = await calc.getResult();
|
|
16
|
+
if (!result) {
|
|
17
|
+
deps.logger.log('INFO', `${name} for ${dStr}: Skipped (Empty Result)`);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const mainDocRef = deps.db.collection(config.resultsCollection)
|
|
22
|
+
.doc(dStr)
|
|
23
|
+
.collection(config.resultsSubcollection)
|
|
24
|
+
.doc(calc.manifest.category)
|
|
25
|
+
.collection(config.computationsSubcollection)
|
|
26
|
+
.doc(name);
|
|
27
|
+
|
|
28
|
+
const updates = await prepareAutoShardedWrites(result, mainDocRef, deps.logger);
|
|
29
|
+
|
|
30
|
+
if (calc.manifest.class.getSchema) {
|
|
31
|
+
const { class: _cls, ...safeMetadata } = calc.manifest;
|
|
32
|
+
schemas.push({
|
|
33
|
+
name,
|
|
34
|
+
category: calc.manifest.category,
|
|
35
|
+
schema: calc.manifest.class.getSchema(),
|
|
36
|
+
metadata: safeMetadata
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (updates.length > 0) {
|
|
41
|
+
await commitBatchInChunks(config, deps, updates, `${name} Results`);
|
|
42
|
+
successUpdates[name] = calc.manifest.hash || true;
|
|
43
|
+
const isSharded = updates.some(u => u.data._sharded === true);
|
|
44
|
+
deps.logger.log('INFO', `${name} for ${dStr}: \u2714 Success (Written ${isSharded ? 'Sharded' : 'Standard'})`);
|
|
45
|
+
} else {
|
|
46
|
+
deps.logger.log('INFO', `${name} for ${dStr}: - Empty Data`);
|
|
47
|
+
}
|
|
48
|
+
} catch (e) {
|
|
49
|
+
deps.logger.log('ERROR', `${name} for ${dStr}: \u2716 FAILED Commit: ${e.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (schemas.length) batchStoreSchemas(deps, config, schemas).catch(() => {});
|
|
54
|
+
|
|
55
|
+
if (!skipStatusWrite && Object.keys(successUpdates).length > 0) {
|
|
56
|
+
await updateComputationStatus(dStr, successUpdates, config, deps);
|
|
57
|
+
deps.logger.log('INFO', `[${passName}] Updated status document for ${Object.keys(successUpdates).length} successful computations.`);
|
|
58
|
+
}
|
|
59
|
+
return successUpdates;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function calculateFirestoreBytes(value) {
|
|
63
|
+
if (value === null) return 1;
|
|
64
|
+
if (value === undefined) return 0;
|
|
65
|
+
if (typeof value === 'boolean') return 1;
|
|
66
|
+
if (typeof value === 'number') return 8;
|
|
67
|
+
if (typeof value === 'string') return Buffer.byteLength(value, 'utf8') + 1;
|
|
68
|
+
if (value instanceof Date) return 8;
|
|
69
|
+
if (value.constructor && value.constructor.name === 'DocumentReference') { return Buffer.byteLength(value.path, 'utf8') + 16; }
|
|
70
|
+
if (Array.isArray(value)) { let sum = 0; for (const item of value) sum += calculateFirestoreBytes(item); return sum; }
|
|
71
|
+
if (typeof value === 'object') { let sum = 0; for (const k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { sum += (Buffer.byteLength(k, 'utf8') + 1) + calculateFirestoreBytes(value[k]); } } return sum; }
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function prepareAutoShardedWrites(result, docRef, logger) {
|
|
76
|
+
const SAFETY_THRESHOLD_BYTES = 1000 * 1024; // 1MB Limit
|
|
77
|
+
const OVERHEAD_ALLOWANCE = 20 * 1024;
|
|
78
|
+
const CHUNK_LIMIT = SAFETY_THRESHOLD_BYTES - OVERHEAD_ALLOWANCE;
|
|
79
|
+
const totalSize = calculateFirestoreBytes(result);
|
|
80
|
+
const docPathSize = Buffer.byteLength(docRef.path, 'utf8') + 16;
|
|
81
|
+
|
|
82
|
+
if ((totalSize + docPathSize) < CHUNK_LIMIT) {
|
|
83
|
+
const data = { ...result, _completed: true, _sharded: false };
|
|
84
|
+
return [{ ref: docRef, data, options: { merge: true } }];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
logger.log('INFO', `[AutoShard] Result size ~${Math.round(totalSize/1024)}KB exceeds limit. Sharding...`);
|
|
88
|
+
const writes = [];
|
|
89
|
+
const shardCollection = docRef.collection('_shards');
|
|
90
|
+
let currentChunk = {};
|
|
91
|
+
let currentChunkSize = 0;
|
|
92
|
+
let shardIndex = 0;
|
|
93
|
+
|
|
94
|
+
for (const [key, value] of Object.entries(result)) {
|
|
95
|
+
if (key.startsWith('_')) continue;
|
|
96
|
+
const keySize = Buffer.byteLength(key, 'utf8') + 1;
|
|
97
|
+
const valueSize = calculateFirestoreBytes(value);
|
|
98
|
+
const itemSize = keySize + valueSize;
|
|
99
|
+
|
|
100
|
+
if (currentChunkSize + itemSize > CHUNK_LIMIT) {
|
|
101
|
+
writes.push({ ref: shardCollection.doc(`shard_${shardIndex}`), data: currentChunk, options: { merge: false } });
|
|
102
|
+
shardIndex++;
|
|
103
|
+
currentChunk = {};
|
|
104
|
+
currentChunkSize = 0;
|
|
105
|
+
}
|
|
106
|
+
currentChunk[key] = value;
|
|
107
|
+
currentChunkSize += itemSize;
|
|
108
|
+
}
|
|
109
|
+
if (Object.keys(currentChunk).length > 0) {
|
|
110
|
+
writes.push({ ref: shardCollection.doc(`shard_${shardIndex}`), data: currentChunk, options: { merge: false } });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const pointerData = { _completed: true, _sharded: true, _shardCount: shardIndex + 1, _lastUpdated: new Date().toISOString() };
|
|
114
|
+
writes.push({ ref: docRef, data: pointerData, options: { merge: false } });
|
|
115
|
+
return writes;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { commitResults };
|