bulltrackers-module 1.0.181 → 1.0.183
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/controllers/computation_controller.js +62 -7
- package/functions/computation-system/helpers/computation_pass_runner.js +53 -57
- package/functions/computation-system/helpers/orchestration_helpers.js +27 -41
- package/functions/task-engine/utils/firestore_batch_manager.js +17 -43
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FIXED: computation_controller.js
|
|
3
|
-
* V3.
|
|
3
|
+
* V3.3: Adds Price Loading for Meta Context & Fixes Context Injection
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { DataExtractor,
|
|
@@ -23,7 +23,7 @@ class DataLoader {
|
|
|
23
23
|
constructor(config, dependencies) {
|
|
24
24
|
this.config = config;
|
|
25
25
|
this.deps = dependencies;
|
|
26
|
-
this.cache = { mappings: null, insights: new Map(), social: new Map() };
|
|
26
|
+
this.cache = { mappings: null, insights: new Map(), social: new Map(), prices: null };
|
|
27
27
|
}
|
|
28
28
|
async loadMappings() {
|
|
29
29
|
if (this.cache.mappings) return this.cache.mappings;
|
|
@@ -43,6 +43,48 @@ class DataLoader {
|
|
|
43
43
|
this.cache.social.set(dateStr, social);
|
|
44
44
|
return social;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* NEW: Loads sharded price data for Meta calculations
|
|
48
|
+
*/
|
|
49
|
+
async loadPrices() {
|
|
50
|
+
if (this.cache.prices) return this.cache.prices;
|
|
51
|
+
const { db, logger } = this.deps;
|
|
52
|
+
const collection = this.config.priceCollection || 'asset_prices';
|
|
53
|
+
|
|
54
|
+
logger.log('INFO', `[DataLoader] Loading all price shards from ${collection}...`);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const snapshot = await db.collection(collection).get();
|
|
58
|
+
if (snapshot.empty) return { history: [] };
|
|
59
|
+
|
|
60
|
+
// Flatten shards into a single array for the context
|
|
61
|
+
// Structure expected by calculation: Array of { instrumentId, prices: {...} }
|
|
62
|
+
const allPrices = [];
|
|
63
|
+
|
|
64
|
+
snapshot.forEach(doc => {
|
|
65
|
+
const shardData = doc.data();
|
|
66
|
+
// Iterate keys in shard (instrumentIds)
|
|
67
|
+
for (const [instId, data] of Object.entries(shardData)) {
|
|
68
|
+
if (data && data.prices) {
|
|
69
|
+
allPrices.push({
|
|
70
|
+
instrumentId: instId,
|
|
71
|
+
...data
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
logger.log('INFO', `[DataLoader] Loaded prices for ${allPrices.length} instruments.`);
|
|
78
|
+
|
|
79
|
+
// Cache as an object with 'history' array to match context expectations
|
|
80
|
+
this.cache.prices = { history: allPrices };
|
|
81
|
+
return this.cache.prices;
|
|
82
|
+
|
|
83
|
+
} catch (e) {
|
|
84
|
+
logger.log('ERROR', `[DataLoader] Failed to load prices: ${e.message}`);
|
|
85
|
+
return { history: [] };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
46
88
|
}
|
|
47
89
|
|
|
48
90
|
class ContextBuilder {
|
|
@@ -76,7 +118,7 @@ class ContextBuilder {
|
|
|
76
118
|
insights: { today: insights?.today, yesterday: insights?.yesterday },
|
|
77
119
|
social: { today: socialData?.today, yesterday: socialData?.yesterday },
|
|
78
120
|
mappings: mappings || {},
|
|
79
|
-
math: {
|
|
121
|
+
math: {
|
|
80
122
|
extract: DataExtractor,
|
|
81
123
|
history: HistoryExtractor,
|
|
82
124
|
compute: MathPrimitives,
|
|
@@ -103,6 +145,7 @@ class ContextBuilder {
|
|
|
103
145
|
mappings,
|
|
104
146
|
insights,
|
|
105
147
|
socialData,
|
|
148
|
+
prices, // <--- ADDED THIS
|
|
106
149
|
computedDependencies,
|
|
107
150
|
previousComputedDependencies,
|
|
108
151
|
config,
|
|
@@ -113,8 +156,9 @@ class ContextBuilder {
|
|
|
113
156
|
date: { today: dateStr },
|
|
114
157
|
insights: { today: insights?.today, yesterday: insights?.yesterday },
|
|
115
158
|
social: { today: socialData?.today, yesterday: socialData?.yesterday },
|
|
159
|
+
prices: prices || {}, // <--- INJECTED HERE
|
|
116
160
|
mappings: mappings || {},
|
|
117
|
-
math: {
|
|
161
|
+
math: {
|
|
118
162
|
extract: DataExtractor,
|
|
119
163
|
history: HistoryExtractor,
|
|
120
164
|
compute: MathPrimitives,
|
|
@@ -142,22 +186,22 @@ class ComputationExecutor {
|
|
|
142
186
|
this.loader = dataLoader;
|
|
143
187
|
}
|
|
144
188
|
|
|
145
|
-
/**
|
|
146
|
-
* UPDATED: Accepts yesterdayPortfolioData AND historyData separately.
|
|
147
|
-
*/
|
|
148
189
|
async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps) {
|
|
149
190
|
const { logger } = this.deps;
|
|
150
191
|
const targetUserType = metadata.userType;
|
|
151
192
|
const mappings = await this.loader.loadMappings();
|
|
152
193
|
const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
|
|
194
|
+
|
|
153
195
|
for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
|
|
154
196
|
const yesterdayPortfolio = yesterdayPortfolioData ? yesterdayPortfolioData[userId] : null;
|
|
155
197
|
const todayHistory = historyData ? historyData[userId] : null;
|
|
156
198
|
const actualUserType = todayPortfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
|
|
199
|
+
|
|
157
200
|
if (targetUserType !== 'all') {
|
|
158
201
|
const mappedTarget = (targetUserType === 'speculator') ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
|
|
159
202
|
if (mappedTarget !== actualUserType) continue;
|
|
160
203
|
}
|
|
204
|
+
|
|
161
205
|
const context = ContextBuilder.buildPerUserContext({
|
|
162
206
|
todayPortfolio, yesterdayPortfolio,
|
|
163
207
|
todayHistory,
|
|
@@ -166,6 +210,7 @@ class ComputationExecutor {
|
|
|
166
210
|
previousComputedDependencies: prevDeps,
|
|
167
211
|
config: this.config, deps: this.deps
|
|
168
212
|
});
|
|
213
|
+
|
|
169
214
|
try { await calcInstance.process(context); }
|
|
170
215
|
catch (e) { logger.log('WARN', `Calc ${metadata.name} failed for user ${userId}: ${e.message}`); }
|
|
171
216
|
}
|
|
@@ -173,10 +218,20 @@ class ComputationExecutor {
|
|
|
173
218
|
|
|
174
219
|
async executeOncePerDay(calcInstance, metadata, dateStr, computedDeps, prevDeps) {
|
|
175
220
|
const mappings = await this.loader.loadMappings();
|
|
221
|
+
|
|
222
|
+
// Load standard dependencies
|
|
176
223
|
const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
|
|
177
224
|
const social = metadata.rootDataDependencies?.includes('social') ? { today: await this.loader.loadSocial(dateStr) } : null;
|
|
225
|
+
|
|
226
|
+
// NEW: Load Price dependencies if required
|
|
227
|
+
let prices = null;
|
|
228
|
+
if (metadata.rootDataDependencies?.includes('price')) {
|
|
229
|
+
prices = await this.loader.loadPrices();
|
|
230
|
+
}
|
|
231
|
+
|
|
178
232
|
const context = ContextBuilder.buildMetaContext({
|
|
179
233
|
dateStr, metadata, mappings, insights, socialData: social,
|
|
234
|
+
prices, // Pass prices to builder
|
|
180
235
|
computedDependencies: computedDeps,
|
|
181
236
|
previousComputedDependencies: prevDeps,
|
|
182
237
|
config: this.config, deps: this.deps
|
|
@@ -6,13 +6,15 @@ const {
|
|
|
6
6
|
groupByPass,
|
|
7
7
|
checkRootDataAvailability,
|
|
8
8
|
fetchExistingResults,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
fetchComputationStatus,
|
|
10
|
+
updateComputationStatus,
|
|
11
11
|
runStandardComputationPass,
|
|
12
12
|
runMetaComputationPass,
|
|
13
13
|
checkRootDependencies
|
|
14
14
|
} = require('./orchestration_helpers.js');
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
const { getExpectedDateStrings, normalizeName } = require('../utils/utils.js');
|
|
17
|
+
|
|
16
18
|
const PARALLEL_BATCH_SIZE = 7;
|
|
17
19
|
|
|
18
20
|
async function runComputationPass(config, dependencies, computationManifest) {
|
|
@@ -21,7 +23,7 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
21
23
|
if (!passToRun)
|
|
22
24
|
return logger.log('ERROR', '[PassRunner] No pass defined. Aborting.');
|
|
23
25
|
|
|
24
|
-
logger.log('INFO', `🚀 Starting PASS ${passToRun}
|
|
26
|
+
logger.log('INFO', `🚀 Starting PASS ${passToRun} (Targeting /computation_status/{YYYY-MM-DD})...`);
|
|
25
27
|
|
|
26
28
|
// Hardcoded earliest dates
|
|
27
29
|
const earliestDates = { portfolio: new Date('2025-09-25T00:00:00Z'), history: new Date('2025-11-05T00:00:00Z'), social: new Date('2025-10-30T00:00:00Z'), insights: new Date('2025-08-26T00:00:00Z') };
|
|
@@ -34,42 +36,53 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
34
36
|
return logger.log('WARN', `[PassRunner] No calcs for Pass ${passToRun}. Exiting.`);
|
|
35
37
|
|
|
36
38
|
const passEarliestDate = earliestDates.absoluteEarliest;
|
|
37
|
-
const endDateUTC
|
|
39
|
+
const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
|
|
38
40
|
const allExpectedDates = getExpectedDateStrings(passEarliestDate, endDateUTC);
|
|
39
41
|
|
|
40
42
|
const standardCalcs = calcsInThisPass.filter(c => c.type === 'standard');
|
|
41
43
|
const metaCalcs = calcsInThisPass.filter(c => c.type === 'meta');
|
|
42
44
|
|
|
43
|
-
//
|
|
44
|
-
// Returns { "2023-10-27": { calcA: true, calcB: false }, ... }
|
|
45
|
-
const globalStatusData = await fetchGlobalComputationStatus(config, dependencies);
|
|
46
|
-
|
|
47
|
-
// Helper: Check status using in-memory data
|
|
48
|
-
const shouldRun = (calc, dateStr) => {
|
|
49
|
-
const dailyStatus = globalStatusData[dateStr] || {};
|
|
50
|
-
|
|
51
|
-
// 1. If explicitly TRUE, ignore.
|
|
52
|
-
if (dailyStatus[calc.name] === true) return false;
|
|
53
|
-
|
|
54
|
-
// 2. Check dependencies (using same in-memory status)
|
|
55
|
-
if (calc.dependencies && calc.dependencies.length > 0) {
|
|
56
|
-
const depsMet = calc.dependencies.every(depName => dailyStatus[depName] === true);
|
|
57
|
-
if (!depsMet) return false;
|
|
58
|
-
}
|
|
59
|
-
return true;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// Process a single date and RETURN updates (do not write)
|
|
45
|
+
// Process a single date
|
|
63
46
|
const processDate = async (dateStr) => {
|
|
64
47
|
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
65
|
-
|
|
66
|
-
|
|
48
|
+
|
|
49
|
+
// 1. Fetch Status for THIS specific date only
|
|
50
|
+
// This ensures Pass 2 sees exactly what Pass 1 wrote for this date.
|
|
51
|
+
const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
|
|
52
|
+
|
|
53
|
+
// Helper: Check status using the fetched daily data
|
|
54
|
+
const shouldRun = (calc) => {
|
|
55
|
+
const cName = normalizeName(calc.name);
|
|
56
|
+
|
|
57
|
+
// A. If recorded as TRUE -> Ignore (already ran)
|
|
58
|
+
if (dailyStatus[cName] === true) return false;
|
|
59
|
+
|
|
60
|
+
// B. If recorded as FALSE or UNDEFINED -> Run it (retry or new)
|
|
61
|
+
// But first, check if we have the necessary data dependencies.
|
|
62
|
+
|
|
63
|
+
if (calc.dependencies && calc.dependencies.length > 0) {
|
|
64
|
+
// Check if prerequisites (from previous passes on THIS date) are complete
|
|
65
|
+
const missing = calc.dependencies.filter(depName => dailyStatus[normalizeName(depName)] !== true);
|
|
66
|
+
if (missing.length > 0) {
|
|
67
|
+
// Dependency missing: cannot run yet.
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// If we are here, status is false/undefined AND dependencies are met.
|
|
73
|
+
return true;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const standardToRun = standardCalcs.filter(shouldRun);
|
|
77
|
+
const metaToRun = metaCalcs.filter(shouldRun);
|
|
67
78
|
|
|
68
|
-
if (!standardToRun.length && !metaToRun.length) return null; // No work
|
|
79
|
+
if (!standardToRun.length && !metaToRun.length) return null; // No work for this date
|
|
69
80
|
|
|
81
|
+
// 2. Check Root Data Availability (Portfolio, History, etc.)
|
|
70
82
|
const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
|
|
71
83
|
if (!rootData) return null;
|
|
72
84
|
|
|
85
|
+
// 3. Filter again based on Root Data availability
|
|
73
86
|
const finalStandardToRun = standardToRun.filter(c => checkRootDependencies(c, rootData.status).canRun);
|
|
74
87
|
const finalMetaToRun = metaToRun.filter(c => checkRootDependencies(c, rootData.status).canRun);
|
|
75
88
|
|
|
@@ -85,52 +98,35 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
85
98
|
const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
86
99
|
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
87
100
|
const previousResults = await fetchExistingResults(prevDateStr, calcsRunning, computationManifest, config, dependencies, true);
|
|
101
|
+
|
|
102
|
+
// Note: We use skipStatusWrite=true because we want to batch write the status at the end of this function
|
|
88
103
|
if (finalStandardToRun.length) {
|
|
89
|
-
const updates = await runStandardComputationPass(dateToProcess, finalStandardToRun, `Pass ${passToRun} (Std)`, config, dependencies, rootData, existingResults, previousResults, true);
|
|
104
|
+
const updates = await runStandardComputationPass(dateToProcess, finalStandardToRun, `Pass ${passToRun} (Std)`, config, dependencies, rootData, existingResults, previousResults, true);
|
|
90
105
|
Object.assign(dateUpdates, updates);
|
|
91
106
|
}
|
|
92
107
|
if (finalMetaToRun.length) {
|
|
93
|
-
const updates = await runMetaComputationPass(dateToProcess, finalMetaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData, true);
|
|
108
|
+
const updates = await runMetaComputationPass(dateToProcess, finalMetaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData, true);
|
|
94
109
|
Object.assign(dateUpdates, updates);
|
|
95
110
|
}
|
|
96
111
|
} catch (err) {
|
|
97
112
|
logger.log('ERROR', `[PassRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message });
|
|
98
|
-
|
|
99
|
-
|
|
113
|
+
[...finalStandardToRun, ...finalMetaToRun].forEach(c => dateUpdates[normalizeName(c.name)] = false);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 4. Write "true" or "false" results for THIS specific date immediately
|
|
117
|
+
if (Object.keys(dateUpdates).length > 0) {
|
|
118
|
+
await updateComputationStatus(dateStr, dateUpdates, config, dependencies);
|
|
100
119
|
}
|
|
101
120
|
|
|
102
|
-
// Return the updates for this date
|
|
103
121
|
return { date: dateStr, updates: dateUpdates };
|
|
104
122
|
};
|
|
105
123
|
|
|
106
124
|
// Batch process dates
|
|
107
125
|
for (let i = 0; i < allExpectedDates.length; i += PARALLEL_BATCH_SIZE) {
|
|
108
126
|
const batch = allExpectedDates.slice(i, i + PARALLEL_BATCH_SIZE);
|
|
109
|
-
|
|
110
|
-
// Run batch in parallel
|
|
111
|
-
const results = await Promise.all(batch.map(processDate));
|
|
112
|
-
|
|
113
|
-
// Aggregate updates from the batch
|
|
114
|
-
const batchUpdates = {};
|
|
115
|
-
let hasUpdates = false;
|
|
116
|
-
|
|
117
|
-
results.forEach(res => {
|
|
118
|
-
if (res && res.updates && Object.keys(res.updates).length > 0) {
|
|
119
|
-
batchUpdates[res.date] = res.updates;
|
|
120
|
-
hasUpdates = true;
|
|
121
|
-
|
|
122
|
-
// Also update our local in-memory copy so subsequent logic in this run sees it (though passes usually rely on prev days)
|
|
123
|
-
if (!globalStatusData[res.date]) globalStatusData[res.date] = {};
|
|
124
|
-
Object.assign(globalStatusData[res.date], res.updates);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Write status ONCE per batch
|
|
129
|
-
if (hasUpdates) {
|
|
130
|
-
await updateGlobalComputationStatus(batchUpdates, config, dependencies);
|
|
131
|
-
logger.log('INFO', `[PassRunner] Batched status update for ${Object.keys(batchUpdates).length} dates.`);
|
|
132
|
-
}
|
|
127
|
+
await Promise.all(batch.map(processDate));
|
|
133
128
|
}
|
|
129
|
+
|
|
134
130
|
logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
|
|
135
131
|
}
|
|
136
132
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FILENAME: bulltrackers-module/functions/computation-system/helpers/orchestration_helpers.js
|
|
3
|
+
* FIXED: Only marks computations as TRUE if they actually store results.
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
const { ComputationController } = require('../controllers/computation_controller');
|
|
@@ -27,6 +28,8 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
|
|
|
27
28
|
else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
|
|
28
29
|
else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
|
|
29
30
|
else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history');
|
|
31
|
+
// Note: 'price' is typically not a blocking root check in this specific function logic unless added,
|
|
32
|
+
// but usually prices are treated as auxiliary. If you want to block on prices, add it here.
|
|
30
33
|
}
|
|
31
34
|
return { canRun: missing.length === 0, missing };
|
|
32
35
|
}
|
|
@@ -35,7 +38,6 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
|
|
|
35
38
|
* Checks for the availability of all required root data for a specific date.
|
|
36
39
|
*/
|
|
37
40
|
async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
|
|
38
|
-
// ... [Unchanged content of checkRootDataAvailability] ...
|
|
39
41
|
const { logger } = dependencies;
|
|
40
42
|
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
41
43
|
let portfolioRefs = [], historyRefs = [];
|
|
@@ -50,6 +52,7 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
|
|
|
50
52
|
|
|
51
53
|
await Promise.all(tasks);
|
|
52
54
|
|
|
55
|
+
// We allow running if ANY data is present. Specific calcs filter themselves using checkRootDependencies.
|
|
53
56
|
if (!(hasPortfolio || hasInsights || hasSocial || hasHistory)) return null;
|
|
54
57
|
|
|
55
58
|
return {
|
|
@@ -64,10 +67,6 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
|
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
|
|
67
|
-
/**
|
|
68
|
-
* --- DEPRECATED: Old per-date fetch ---
|
|
69
|
-
* Keeps compatibility but logic moves to fetchGlobalComputationStatus
|
|
70
|
-
*/
|
|
71
70
|
async function fetchComputationStatus(dateStr, config, { db }) {
|
|
72
71
|
const collection = config.computationStatusCollection || 'computation_status';
|
|
73
72
|
const docRef = db.collection(collection).doc(dateStr);
|
|
@@ -75,10 +74,6 @@ async function fetchComputationStatus(dateStr, config, { db }) {
|
|
|
75
74
|
return snap.exists ? snap.data() : {};
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
/**
|
|
79
|
-
* --- NEW: Fetches the SINGLE GLOBAL status document ---
|
|
80
|
-
* Loads the entire history of statuses in one read.
|
|
81
|
-
*/
|
|
82
77
|
async function fetchGlobalComputationStatus(config, { db }) {
|
|
83
78
|
const collection = config.computationStatusCollection || 'computation_status';
|
|
84
79
|
const docRef = db.collection(collection).doc('global_status');
|
|
@@ -86,9 +81,6 @@ async function fetchGlobalComputationStatus(config, { db }) {
|
|
|
86
81
|
return snap.exists ? snap.data() : {};
|
|
87
82
|
}
|
|
88
83
|
|
|
89
|
-
/**
|
|
90
|
-
* --- DEPRECATED: Old per-date update ---
|
|
91
|
-
*/
|
|
92
84
|
async function updateComputationStatus(dateStr, updates, config, { db }) {
|
|
93
85
|
if (!updates || Object.keys(updates).length === 0) return;
|
|
94
86
|
const collection = config.computationStatusCollection || 'computation_status';
|
|
@@ -96,17 +88,11 @@ async function updateComputationStatus(dateStr, updates, config, { db }) {
|
|
|
96
88
|
await docRef.set(updates, { merge: true });
|
|
97
89
|
}
|
|
98
90
|
|
|
99
|
-
/**
|
|
100
|
-
* --- NEW: Batch Updates to Global Document ---
|
|
101
|
-
* Accepts a map of { "YYYY-MM-DD": { calcName: true, ... } }
|
|
102
|
-
* and writes them using dot notation to avoid overwriting other dates.
|
|
103
|
-
*/
|
|
104
91
|
async function updateGlobalComputationStatus(updatesByDate, config, { db }) {
|
|
105
92
|
if (!updatesByDate || Object.keys(updatesByDate).length === 0) return;
|
|
106
93
|
const collection = config.computationStatusCollection || 'computation_status';
|
|
107
94
|
const docRef = db.collection(collection).doc('global_status');
|
|
108
95
|
|
|
109
|
-
// Flatten to dot notation for Firestore update: "2023-10-27.calcName": true
|
|
110
96
|
const flattenUpdates = {};
|
|
111
97
|
for (const [date, statuses] of Object.entries(updatesByDate)) {
|
|
112
98
|
for (const [calc, status] of Object.entries(statuses)) {
|
|
@@ -117,7 +103,6 @@ async function updateGlobalComputationStatus(updatesByDate, config, { db }) {
|
|
|
117
103
|
try {
|
|
118
104
|
await docRef.update(flattenUpdates);
|
|
119
105
|
} catch (err) {
|
|
120
|
-
// If doc doesn't exist (first run), update fails. Use set({merge:true}).
|
|
121
106
|
if (err.code === 5) { // NOT_FOUND
|
|
122
107
|
const deepObj = {};
|
|
123
108
|
for (const [date, statuses] of Object.entries(updatesByDate)) {
|
|
@@ -130,10 +115,6 @@ async function updateGlobalComputationStatus(updatesByDate, config, { db }) {
|
|
|
130
115
|
}
|
|
131
116
|
}
|
|
132
117
|
|
|
133
|
-
/**
|
|
134
|
-
* --- UPDATED: fetchExistingResults ---
|
|
135
|
-
* (Unchanged, keeps fetching results per date as this is heavy data)
|
|
136
|
-
*/
|
|
137
118
|
async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config, { db }, includeSelf = false) {
|
|
138
119
|
const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
|
|
139
120
|
const calcsToFetch = new Set();
|
|
@@ -158,10 +139,6 @@ async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config,
|
|
|
158
139
|
return fetched;
|
|
159
140
|
}
|
|
160
141
|
|
|
161
|
-
/**
|
|
162
|
-
* --- UPDATED: streamAndProcess ---
|
|
163
|
-
* (Unchanged)
|
|
164
|
-
*/
|
|
165
142
|
async function streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs, fetchedDeps, previousFetchedDeps) {
|
|
166
143
|
const { logger } = deps;
|
|
167
144
|
const controller = new ComputationController(config, deps);
|
|
@@ -211,10 +188,6 @@ async function streamAndProcess(dateStr, state, passName, config, deps, rootData
|
|
|
211
188
|
logger.log('INFO', `[${passName}] Streaming complete.`);
|
|
212
189
|
}
|
|
213
190
|
|
|
214
|
-
/**
|
|
215
|
-
* --- UPDATED: runStandardComputationPass ---
|
|
216
|
-
* Now accepts `skipStatusWrite` and returns `successUpdates`
|
|
217
|
-
*/
|
|
218
191
|
async function runStandardComputationPass(date, calcs, passName, config, deps, rootData, fetchedDeps, previousFetchedDeps, skipStatusWrite = false) {
|
|
219
192
|
const dStr = date.toISOString().slice(0, 10);
|
|
220
193
|
const logger = deps.logger;
|
|
@@ -240,14 +213,9 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
|
|
|
240
213
|
|
|
241
214
|
await streamAndProcess(dStr, state, passName, config, deps, fullRoot, rootData.portfolioRefs, rootData.historyRefs, fetchedDeps, previousFetchedDeps);
|
|
242
215
|
|
|
243
|
-
// Return the updates instead of just writing them
|
|
244
216
|
return await commitResults(state, dStr, passName, config, deps, skipStatusWrite);
|
|
245
217
|
}
|
|
246
218
|
|
|
247
|
-
/**
|
|
248
|
-
* --- UPDATED: runMetaComputationPass ---
|
|
249
|
-
* Now accepts `skipStatusWrite` and returns `successUpdates`
|
|
250
|
-
*/
|
|
251
219
|
async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, previousFetchedDeps, rootData, skipStatusWrite = false) {
|
|
252
220
|
const controller = new ComputationController(config, deps);
|
|
253
221
|
const dStr = date.toISOString().slice(0, 10);
|
|
@@ -266,8 +234,8 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
|
|
|
266
234
|
}
|
|
267
235
|
|
|
268
236
|
/**
|
|
269
|
-
* ---
|
|
270
|
-
*
|
|
237
|
+
* --- FIXED: commitResults ---
|
|
238
|
+
* Only marks 'successUpdates' if data is actually written.
|
|
271
239
|
*/
|
|
272
240
|
async function commitResults(stateObj, dStr, passName, config, deps, skipStatusWrite = false) {
|
|
273
241
|
const writes = [], schemas = [], sharded = {};
|
|
@@ -278,13 +246,23 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
278
246
|
try {
|
|
279
247
|
const result = await calc.getResult();
|
|
280
248
|
if (!result) continue;
|
|
249
|
+
|
|
281
250
|
const standardRes = {};
|
|
251
|
+
let hasData = false; // Track if this calc produced data
|
|
252
|
+
|
|
282
253
|
for (const key in result) {
|
|
283
254
|
if (key.startsWith('sharded_')) {
|
|
284
255
|
const sData = result[key];
|
|
285
|
-
for (const c in sData) {
|
|
286
|
-
|
|
256
|
+
for (const c in sData) {
|
|
257
|
+
sharded[c] = sharded[c] || {};
|
|
258
|
+
Object.assign(sharded[c], sData[c]);
|
|
259
|
+
}
|
|
260
|
+
if (Object.keys(sData).length > 0) hasData = true;
|
|
261
|
+
} else {
|
|
262
|
+
standardRes[key] = result[key];
|
|
263
|
+
}
|
|
287
264
|
}
|
|
265
|
+
|
|
288
266
|
if (Object.keys(standardRes).length) {
|
|
289
267
|
standardRes._completed = true;
|
|
290
268
|
writes.push({
|
|
@@ -293,7 +271,9 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
293
271
|
.collection(config.computationsSubcollection).doc(name),
|
|
294
272
|
data: standardRes
|
|
295
273
|
});
|
|
274
|
+
hasData = true;
|
|
296
275
|
}
|
|
276
|
+
|
|
297
277
|
if (calc.manifest.class.getSchema) {
|
|
298
278
|
const { class: _cls, ...safeMetadata } = calc.manifest;
|
|
299
279
|
schemas.push({
|
|
@@ -304,7 +284,13 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
304
284
|
});
|
|
305
285
|
}
|
|
306
286
|
|
|
307
|
-
|
|
287
|
+
// FIX: Only mark as successful if we actually had data to write.
|
|
288
|
+
if (hasData) {
|
|
289
|
+
successUpdates[name] = true;
|
|
290
|
+
} else {
|
|
291
|
+
// Optional: Log that it produced no data
|
|
292
|
+
// deps.logger.log('INFO', `Calc ${name} produced no data. Skipping status update.`);
|
|
293
|
+
}
|
|
308
294
|
|
|
309
295
|
} catch (e) { deps.logger.log('ERROR', `Commit failed ${name}: ${e.message}`); }
|
|
310
296
|
}
|
|
@@ -10,30 +10,31 @@ const { FieldValue } = require('@google-cloud/firestore');
|
|
|
10
10
|
|
|
11
11
|
class FirestoreBatchManager {
|
|
12
12
|
constructor(db, headerManager, logger, config) {
|
|
13
|
-
this.db
|
|
13
|
+
this.db = db;
|
|
14
14
|
this.headerManager = headerManager;
|
|
15
|
-
this.logger
|
|
16
|
-
this.config
|
|
17
|
-
this.portfolioBatch
|
|
18
|
-
this.timestampBatch
|
|
19
|
-
this.tradingHistoryBatch
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.portfolioBatch = {};
|
|
18
|
+
this.timestampBatch = {};
|
|
19
|
+
this.tradingHistoryBatch = {};
|
|
20
20
|
this.speculatorTimestampFixBatch = {};
|
|
21
21
|
|
|
22
22
|
// Username map cache
|
|
23
|
-
this.usernameMap
|
|
24
|
-
this.usernameMapUpdates
|
|
23
|
+
this.usernameMap = new Map();
|
|
24
|
+
this.usernameMapUpdates = {};
|
|
25
25
|
this.usernameMapLastLoaded = 0;
|
|
26
26
|
|
|
27
27
|
// History fetch cache (NEW)
|
|
28
28
|
this.historyFetchedUserIds = new Set();
|
|
29
29
|
this.historyCacheTimestamp = Date.now();
|
|
30
|
-
this.HISTORY_CACHE_TTL_MS
|
|
30
|
+
this.HISTORY_CACHE_TTL_MS = config.HISTORY_CACHE_TTL_MS || 600000;
|
|
31
31
|
|
|
32
|
-
this.processedSpeculatorCids
|
|
33
|
-
this.usernameMapCollectionName
|
|
34
|
-
this.normalHistoryCollectionName
|
|
32
|
+
this.processedSpeculatorCids = new Set();
|
|
33
|
+
this.usernameMapCollectionName = config.FIRESTORE_COLLECTION_USERNAME_MAP;
|
|
34
|
+
this.normalHistoryCollectionName = config.FIRESTORE_COLLECTION_NORMAL_HISTORY;
|
|
35
35
|
this.speculatorHistoryCollectionName = config.FIRESTORE_COLLECTION_SPECULATOR_HISTORY;
|
|
36
|
-
this.batchTimeout
|
|
36
|
+
this.batchTimeout = null;
|
|
37
|
+
|
|
37
38
|
logger.log('INFO', 'FirestoreBatchManager initialized.');
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -50,16 +51,10 @@ class FirestoreBatchManager {
|
|
|
50
51
|
|
|
51
52
|
_getUsernameShardId(cid) { return `cid_map_shard_${Math.floor(parseInt(cid) / 10000) % 10}`; }
|
|
52
53
|
|
|
53
|
-
// --- CRITICAL FIX: Removed aggressive timeout flush ---
|
|
54
|
-
// With sequential processing, the timer was firing too often, causing 1 write per user (expensive).
|
|
55
|
-
// Now we only flush if we hit the memory limit (MAX_BATCH_SIZE) or when explicitly called at the end.
|
|
56
54
|
_scheduleFlush() {
|
|
57
55
|
const maxBatch = this.config.TASK_ENGINE_MAX_BATCH_SIZE ? Number(this.config.TASK_ENGINE_MAX_BATCH_SIZE) : 400;
|
|
58
56
|
const totalOps = this._estimateBatchSize();
|
|
59
|
-
if (totalOps >= maxBatch) {
|
|
60
|
-
this.flushBatches();
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
57
|
+
if (totalOps >= maxBatch) { this.flushBatches(); return; }
|
|
63
58
|
}
|
|
64
59
|
|
|
65
60
|
_estimateBatchSize() {
|
|
@@ -144,54 +139,33 @@ class FirestoreBatchManager {
|
|
|
144
139
|
*/
|
|
145
140
|
_flushDataBatch(batchData, firestoreBatch, logName) {
|
|
146
141
|
let count = 0;
|
|
147
|
-
|
|
148
|
-
// 1. Determine Shard Strategy
|
|
149
|
-
// If we expect ~1500 users in a block and want 200 users per shard:
|
|
150
|
-
// We need ceil(1500 / 200) = 8 shards total (part_0 to part_7).
|
|
151
|
-
// Any ID, no matter how random, will map to one of these 8 buckets.
|
|
152
142
|
const TARGET_USERS = this.config.DISCOVERY_ORCHESTRATOR_TARGET_USERS_PER_BLOCK ? Number(this.config.DISCOVERY_ORCHESTRATOR_TARGET_USERS_PER_BLOCK) : 1500;
|
|
153
143
|
const SHARD_CAPACITY = this.config.TASK_ENGINE_MAX_USERS_PER_SHARD ? Number(this.config.TASK_ENGINE_MAX_USERS_PER_SHARD) : 200;
|
|
154
|
-
|
|
155
|
-
// Ensure at least 1 shard exists
|
|
156
144
|
const TOTAL_SHARDS = Math.max(1, Math.ceil(TARGET_USERS / SHARD_CAPACITY));
|
|
157
|
-
|
|
158
145
|
for (const basePath in batchData) {
|
|
159
146
|
const users = batchData[basePath];
|
|
160
147
|
const userIds = Object.keys(users);
|
|
161
148
|
if (!userIds.length) continue;
|
|
162
|
-
|
|
163
149
|
const updatesByShard = {};
|
|
164
|
-
|
|
165
150
|
for (const userId of userIds) {
|
|
166
151
|
const cid = parseInt(userId, 10);
|
|
167
152
|
let shardId;
|
|
168
153
|
|
|
169
154
|
if (!isNaN(cid)) {
|
|
170
|
-
// --- MODULO SHARDING ---
|
|
171
|
-
// Even if IDs are 10, 1000000, 500... they will round-robin into
|
|
172
|
-
// the fixed set of shards (e.g. 8 shards), ensuring density.
|
|
173
155
|
const shardIndex = cid % TOTAL_SHARDS;
|
|
174
156
|
shardId = `part_${shardIndex}`;
|
|
175
|
-
} else {
|
|
176
|
-
shardId = 'part_misc';
|
|
177
|
-
}
|
|
157
|
+
} else { shardId = 'part_misc'; }
|
|
178
158
|
|
|
179
|
-
if (!updatesByShard[shardId]) {
|
|
180
|
-
updatesByShard[shardId] = {};
|
|
181
|
-
}
|
|
159
|
+
if (!updatesByShard[shardId]) { updatesByShard[shardId] = {}; }
|
|
182
160
|
updatesByShard[shardId][userId] = users[userId];
|
|
183
161
|
}
|
|
184
|
-
|
|
185
162
|
for (const shardId in updatesByShard) {
|
|
186
163
|
const chunkData = updatesByShard[shardId];
|
|
187
164
|
const docRef = this.db.collection(`${basePath}/parts`).doc(shardId);
|
|
188
|
-
// merge: true ensures we append to the doc if it was started in a previous batch
|
|
189
165
|
firestoreBatch.set(docRef, chunkData, { merge: true });
|
|
190
166
|
count++;
|
|
191
167
|
}
|
|
192
|
-
|
|
193
168
|
this.logger.log('INFO', `[BATCH] Staged ${userIds.length} ${logName} users into ${Object.keys(updatesByShard).length} buckets (Modulo ${TOTAL_SHARDS}) for ${basePath}.`);
|
|
194
|
-
|
|
195
169
|
delete batchData[basePath];
|
|
196
170
|
}
|
|
197
171
|
return count;
|