bulltrackers-module 1.0.299 → 1.0.301
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,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FILENAME: computation-system/helpers/computation_dispatcher.js
|
|
3
3
|
* PURPOSE: "Smart Dispatcher" - Analyzes state, initializes Run Counters, and dispatches tasks.
|
|
4
|
-
* UPDATED:
|
|
4
|
+
* UPDATED: Optimized Forensics - Only runs on Retries (Attempt > 1) or Single-Day runs.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { getExpectedDateStrings, getEarliestDataDates, normalizeName, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils.js');
|
|
@@ -37,7 +37,7 @@ async function checkCrashForensics(db, date, pass, computationName) {
|
|
|
37
37
|
const lastRSS = data.telemetry.lastMemory.rssMB || 0;
|
|
38
38
|
|
|
39
39
|
if (lastRSS > OOM_THRESHOLD_MB) {
|
|
40
|
-
console.log(`[Dispatcher] 🕵️♀️ Forensics: ${computationName} likely OOM'd at ${lastRSS}MB. Routing to HIGH-MEM.`);
|
|
40
|
+
// console.log(`[Dispatcher] 🕵️♀️ Forensics: ${computationName} likely OOM'd at ${lastRSS}MB. Routing to HIGH-MEM.`);
|
|
41
41
|
return 'high-mem';
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -59,7 +59,7 @@ async function checkCrashForensics(db, date, pass, computationName) {
|
|
|
59
59
|
* @param {Object} config - System config (Injected with topics)
|
|
60
60
|
* @param {Object} dependencies - { db, logger, ... }
|
|
61
61
|
* @param {Array} computationManifest - List of calculations
|
|
62
|
-
* @param {Object} reqBody - (Optional) HTTP Body containing 'callbackUrl' and '
|
|
62
|
+
* @param {Object} reqBody - (Optional) HTTP Body containing 'callbackUrl', 'date', and 'attempt'
|
|
63
63
|
*/
|
|
64
64
|
async function dispatchComputationPass(config, dependencies, computationManifest, reqBody = {}) {
|
|
65
65
|
const { logger, db } = dependencies;
|
|
@@ -67,9 +67,10 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
67
67
|
const passToRun = String(config.COMPUTATION_PASS_TO_RUN);
|
|
68
68
|
|
|
69
69
|
// Extract Date and Callback from request body (pushed by Workflow)
|
|
70
|
-
// NOTE: 'dateStr' acts as the "Target Date" (Ceiling), usually T-1.
|
|
71
70
|
const dateStr = reqBody.date || config.date;
|
|
72
71
|
const callbackUrl = reqBody.callbackUrl || null;
|
|
72
|
+
// [NEW] Get Attempt Count (Default to 1 if missing)
|
|
73
|
+
const attemptCount = reqBody.attempt ? parseInt(reqBody.attempt) : 1;
|
|
73
74
|
|
|
74
75
|
if (!passToRun) { return logger.log('ERROR', '[Dispatcher] No pass defined (COMPUTATION_PASS_TO_RUN). Aborting.'); }
|
|
75
76
|
if (!dateStr) { return logger.log('ERROR', '[Dispatcher] No date defined. Aborting.'); }
|
|
@@ -81,7 +82,7 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
81
82
|
|
|
82
83
|
if (!calcsInThisPass.length) { return logger.log('WARN', `[Dispatcher] No calcs for Pass ${passToRun}. Exiting.`); }
|
|
83
84
|
|
|
84
|
-
logger.log('INFO', `🚀 [Dispatcher] Smart-Dispatching PASS ${passToRun} (Target: ${dateStr})`);
|
|
85
|
+
logger.log('INFO', `🚀 [Dispatcher] Smart-Dispatching PASS ${passToRun} (Target: ${dateStr}) [Attempt ${attemptCount}]`);
|
|
85
86
|
|
|
86
87
|
// -- DATE ANALYSIS LOGIC (FIXED: RANGE SCAN) --
|
|
87
88
|
|
|
@@ -93,7 +94,7 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
93
94
|
// 2. Generate the full range of dates to check
|
|
94
95
|
let allExpectedDates = getExpectedDateStrings(startDate, endDate);
|
|
95
96
|
|
|
96
|
-
// Safety fallback
|
|
97
|
+
// Safety fallback
|
|
97
98
|
if (!allExpectedDates || allExpectedDates.length === 0) {
|
|
98
99
|
logger.log('WARN', `[Dispatcher] Date range calculation returned empty (Start: ${startDate.toISOString()} -> End: ${endDate.toISOString()}). Defaulting to single target date.`);
|
|
99
100
|
allExpectedDates = [dateStr];
|
|
@@ -104,10 +105,25 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
104
105
|
const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
|
|
105
106
|
const tasksToDispatch = [];
|
|
106
107
|
|
|
107
|
-
//
|
|
108
|
-
const
|
|
108
|
+
// [FIX] Separate concurrency limits
|
|
109
|
+
const dateLimit = pLimit(20);
|
|
110
|
+
const forensicsLimit = pLimit(50);
|
|
111
|
+
|
|
112
|
+
// [NEW] SMART FORENSICS TRIGGER
|
|
113
|
+
// 1. If scanning > 5 days (Backfill), SKIP (Too expensive).
|
|
114
|
+
// 2. If attempt == 1 (First Run), SKIP (Assume Standard).
|
|
115
|
+
// 3. Only run if Attempt > 1 AND Small Batch.
|
|
116
|
+
const isBulkBackfill = allExpectedDates.length > 5;
|
|
117
|
+
const shouldRunForensics = (attemptCount > 1) && !isBulkBackfill;
|
|
118
|
+
|
|
119
|
+
if (!shouldRunForensics) {
|
|
120
|
+
if (isBulkBackfill) logger.log('INFO', `[Dispatcher] ⏩ Bulk Backfill (${allExpectedDates.length} days). Skipping Forensics.`);
|
|
121
|
+
else logger.log('INFO', `[Dispatcher] ⏩ First Attempt. Skipping Forensics (Defaulting to Standard).`);
|
|
122
|
+
} else {
|
|
123
|
+
logger.log('WARN', `[Dispatcher] 🕵️♀️ Retry Detected (Attempt ${attemptCount}). Enabling Forensic Crash Analysis.`);
|
|
124
|
+
}
|
|
109
125
|
|
|
110
|
-
const analysisPromises = allExpectedDates.map(d =>
|
|
126
|
+
const analysisPromises = allExpectedDates.map(d => dateLimit(async () => {
|
|
111
127
|
try {
|
|
112
128
|
const fetchPromises = [
|
|
113
129
|
fetchComputationStatus(d, config, dependencies),
|
|
@@ -119,8 +135,6 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
119
135
|
const prevDate = new Date(d + 'T00:00:00Z');
|
|
120
136
|
prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
121
137
|
prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
122
|
-
|
|
123
|
-
// Only fetch previous status if it's within valid range
|
|
124
138
|
if (prevDate >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
|
|
125
139
|
fetchPromises.push(fetchComputationStatus(prevDateStr, config, dependencies));
|
|
126
140
|
}
|
|
@@ -137,7 +151,6 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
137
151
|
|
|
138
152
|
const report = analyzeDateExecution(d, calcsInThisPass, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus);
|
|
139
153
|
|
|
140
|
-
// Handle Status Updates (Impossible / Blocked)
|
|
141
154
|
const statusUpdates = {};
|
|
142
155
|
report.impossible.forEach(item => {
|
|
143
156
|
if (dailyStatus[item.name]?.hash !== STATUS_IMPOSSIBLE) {
|
|
@@ -158,12 +171,14 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
158
171
|
|
|
159
172
|
const validToRun = [...report.runnable, ...report.reRuns];
|
|
160
173
|
|
|
161
|
-
|
|
162
|
-
await Promise.all(validToRun.map(item => limit(async () => {
|
|
174
|
+
await Promise.all(validToRun.map(item => forensicsLimit(async () => {
|
|
163
175
|
const compName = normalizeName(item.name);
|
|
164
176
|
|
|
165
|
-
//
|
|
166
|
-
|
|
177
|
+
// [UPDATED] Conditional Forensics
|
|
178
|
+
let requiredResource = 'standard';
|
|
179
|
+
if (shouldRunForensics) {
|
|
180
|
+
requiredResource = await checkCrashForensics(db, d, passToRun, compName);
|
|
181
|
+
}
|
|
167
182
|
|
|
168
183
|
const uniqueDispatchId = crypto.randomUUID();
|
|
169
184
|
tasksToDispatch.push({
|
|
@@ -177,12 +192,11 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
177
192
|
triggerReason: item.reason || "Unknown",
|
|
178
193
|
dependencyResultHashes: item.dependencyResultHashes || {},
|
|
179
194
|
timestamp: Date.now(),
|
|
180
|
-
resources: requiredResource
|
|
195
|
+
resources: requiredResource
|
|
181
196
|
});
|
|
182
197
|
})));
|
|
183
198
|
|
|
184
|
-
|
|
185
|
-
logger.log('INFO', `[Dispatcher] Analyzed ${d}: ${validToRun.length} tasks identified.`);
|
|
199
|
+
logger.log('INFO', `[Dispatcher] Analyzed ${d}: ${validToRun.length} tasks (Cumulative: ${tasksToDispatch.length})`);
|
|
186
200
|
|
|
187
201
|
} catch (e) {
|
|
188
202
|
logger.log('ERROR', `[Dispatcher] Failed analysis for ${d}: ${e.message}`);
|
|
@@ -195,14 +209,13 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
195
209
|
|
|
196
210
|
if (tasksToDispatch.length > 0) {
|
|
197
211
|
|
|
198
|
-
// 1. Initialize Shared State Document (The Counter)
|
|
199
212
|
const runId = crypto.randomUUID();
|
|
200
213
|
const metaStatePath = `computation_runs/${runId}`;
|
|
201
214
|
|
|
202
215
|
if (callbackUrl) {
|
|
203
216
|
await db.doc(metaStatePath).set({
|
|
204
217
|
createdAt: new Date(),
|
|
205
|
-
date: dateStr,
|
|
218
|
+
date: dateStr,
|
|
206
219
|
pass: passToRun,
|
|
207
220
|
totalTasks: tasksToDispatch.length,
|
|
208
221
|
remainingTasks: tasksToDispatch.length,
|
|
@@ -212,29 +225,23 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
212
225
|
logger.log('INFO', `[Dispatcher] 🏁 Run State Initialized: ${runId}. Tasks: ${tasksToDispatch.length}`);
|
|
213
226
|
}
|
|
214
227
|
|
|
215
|
-
// 2. Attach Run Metadata
|
|
216
228
|
tasksToDispatch.forEach(task => {
|
|
217
229
|
task.runId = runId;
|
|
218
230
|
task.metaStatePath = callbackUrl ? metaStatePath : null;
|
|
219
231
|
});
|
|
220
232
|
|
|
221
|
-
// 3. Create Audit Ledger Entries
|
|
222
233
|
const finalDispatched = [];
|
|
223
|
-
const txnLimit = pLimit(
|
|
234
|
+
const txnLimit = pLimit(50);
|
|
224
235
|
|
|
225
236
|
const txnPromises = tasksToDispatch.map(task => txnLimit(async () => {
|
|
226
237
|
const ledgerRef = db.collection(`computation_audit_ledger/${task.date}/passes/${task.pass}/tasks`).doc(task.computation);
|
|
227
|
-
|
|
228
238
|
try {
|
|
229
239
|
await db.runTransaction(async (t) => {
|
|
230
240
|
const doc = await t.get(ledgerRef);
|
|
231
|
-
|
|
232
241
|
if (doc.exists) {
|
|
233
242
|
const data = doc.data();
|
|
234
|
-
// Strict Idempotency: If completed, don't run again.
|
|
235
243
|
if (data.status === 'COMPLETED') return false;
|
|
236
244
|
}
|
|
237
|
-
|
|
238
245
|
t.set(ledgerRef, {
|
|
239
246
|
status: 'PENDING',
|
|
240
247
|
dispatchId: task.dispatchId,
|
|
@@ -244,15 +251,12 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
244
251
|
createdAt: new Date(),
|
|
245
252
|
dispatcherHash: currentManifestHash,
|
|
246
253
|
triggerReason: task.triggerReason,
|
|
247
|
-
resources: task.resources,
|
|
254
|
+
resources: task.resources,
|
|
248
255
|
retries: 0
|
|
249
256
|
}, { merge: true });
|
|
250
|
-
|
|
251
257
|
return true;
|
|
252
258
|
});
|
|
253
|
-
|
|
254
259
|
finalDispatched.push(task);
|
|
255
|
-
|
|
256
260
|
} catch (txnErr) {
|
|
257
261
|
logger.log('WARN', `[Dispatcher] Transaction failed for ${task.computation}: ${txnErr.message}`);
|
|
258
262
|
}
|
|
@@ -260,13 +264,10 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
260
264
|
|
|
261
265
|
await Promise.all(txnPromises);
|
|
262
266
|
|
|
263
|
-
// 4. Publish to Pub/Sub (Segregated by Resources)
|
|
264
267
|
if (finalDispatched.length > 0) {
|
|
265
|
-
|
|
266
268
|
const standardTasks = finalDispatched.filter(t => t.resources !== 'high-mem');
|
|
267
269
|
const highMemTasks = finalDispatched.filter(t => t.resources === 'high-mem');
|
|
268
270
|
|
|
269
|
-
// Publish Standard
|
|
270
271
|
if (standardTasks.length > 0) {
|
|
271
272
|
logger.log('INFO', `[Dispatcher] ✅ Publishing ${standardTasks.length} Standard tasks...`);
|
|
272
273
|
await pubsubUtils.batchPublishTasks(dependencies, {
|
|
@@ -277,7 +278,6 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
277
278
|
});
|
|
278
279
|
}
|
|
279
280
|
|
|
280
|
-
// Publish High-Mem
|
|
281
281
|
if (highMemTasks.length > 0) {
|
|
282
282
|
logger.log('INFO', `[Dispatcher] 🏋️♀️ Publishing ${highMemTasks.length} tasks to HIGH-MEM infrastructure.`);
|
|
283
283
|
await pubsubUtils.batchPublishTasks(dependencies, {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Cloud Workflows Definition for BullTrackers Computation Pipeline
|
|
2
2
|
# Orchestrates 5 sequential passes using Event-Driven Callbacks (Zero Polling).
|
|
3
|
-
#
|
|
3
|
+
# UPDATED: Passes 'attempt' count to Dispatcher to trigger Smart Forensics on retries.
|
|
4
4
|
|
|
5
5
|
main:
|
|
6
6
|
params: [input]
|
|
@@ -16,7 +16,7 @@ main:
|
|
|
16
16
|
- yesterday_str: ${text.substring(time.format(yesterday_timestamp), 0, 10)}
|
|
17
17
|
- date_to_run: ${default(map.get(input, "date"), yesterday_str)}
|
|
18
18
|
|
|
19
|
-
# Configuration Variables
|
|
19
|
+
# Configuration Variables
|
|
20
20
|
- passes: ["1", "2", "3", "4", "5"]
|
|
21
21
|
- max_retries: 3
|
|
22
22
|
|
|
@@ -71,6 +71,7 @@ main:
|
|
|
71
71
|
body:
|
|
72
72
|
date: ${date_to_run}
|
|
73
73
|
callbackUrl: ${callback_url}
|
|
74
|
+
attempt: ${attempt_count} # [UPDATED] Critical for Smart Forensics logic
|
|
74
75
|
auth:
|
|
75
76
|
type: OIDC
|
|
76
77
|
timeout: 1800 # 30 mins max for dispatch analysis
|
|
@@ -96,7 +97,7 @@ main:
|
|
|
96
97
|
call: events.await_callback
|
|
97
98
|
args:
|
|
98
99
|
callback: ${callback_details}
|
|
99
|
-
timeout: 10800 #
|
|
100
|
+
timeout: 10800 # Reduced to 3h to fail faster if stalled
|
|
100
101
|
result: callback_request
|
|
101
102
|
|
|
102
103
|
# 5. PROCESS SIGNAL
|