bulltrackers-module 1.0.181 → 1.0.182
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.
|
@@ -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') };
|
|
@@ -40,36 +42,47 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
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
|
|
|
@@ -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;
|