bulltrackers-module 1.0.300 → 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');
|
|
@@ -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,9 +105,23 @@ 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
|
-
// [FIX] Separate concurrency limits
|
|
108
|
-
const dateLimit = pLimit(20);
|
|
109
|
-
const forensicsLimit = pLimit(50);
|
|
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
|
+
}
|
|
110
125
|
|
|
111
126
|
const analysisPromises = allExpectedDates.map(d => dateLimit(async () => {
|
|
112
127
|
try {
|
|
@@ -120,8 +135,6 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
120
135
|
const prevDate = new Date(d + 'T00:00:00Z');
|
|
121
136
|
prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
122
137
|
prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
123
|
-
|
|
124
|
-
// Only fetch previous status if it's within valid range
|
|
125
138
|
if (prevDate >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
|
|
126
139
|
fetchPromises.push(fetchComputationStatus(prevDateStr, config, dependencies));
|
|
127
140
|
}
|
|
@@ -138,7 +151,6 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
138
151
|
|
|
139
152
|
const report = analyzeDateExecution(d, calcsInThisPass, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus);
|
|
140
153
|
|
|
141
|
-
// Handle Status Updates (Impossible / Blocked)
|
|
142
154
|
const statusUpdates = {};
|
|
143
155
|
report.impossible.forEach(item => {
|
|
144
156
|
if (dailyStatus[item.name]?.hash !== STATUS_IMPOSSIBLE) {
|
|
@@ -159,12 +171,14 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
159
171
|
|
|
160
172
|
const validToRun = [...report.runnable, ...report.reRuns];
|
|
161
173
|
|
|
162
|
-
// [FIX] Use separate 'forensicsLimit' here to avoid deadlock with 'dateLimit'
|
|
163
174
|
await Promise.all(validToRun.map(item => forensicsLimit(async () => {
|
|
164
175
|
const compName = normalizeName(item.name);
|
|
165
176
|
|
|
166
|
-
//
|
|
167
|
-
|
|
177
|
+
// [UPDATED] Conditional Forensics
|
|
178
|
+
let requiredResource = 'standard';
|
|
179
|
+
if (shouldRunForensics) {
|
|
180
|
+
requiredResource = await checkCrashForensics(db, d, passToRun, compName);
|
|
181
|
+
}
|
|
168
182
|
|
|
169
183
|
const uniqueDispatchId = crypto.randomUUID();
|
|
170
184
|
tasksToDispatch.push({
|
|
@@ -178,11 +192,10 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
178
192
|
triggerReason: item.reason || "Unknown",
|
|
179
193
|
dependencyResultHashes: item.dependencyResultHashes || {},
|
|
180
194
|
timestamp: Date.now(),
|
|
181
|
-
resources: requiredResource
|
|
195
|
+
resources: requiredResource
|
|
182
196
|
});
|
|
183
197
|
})));
|
|
184
198
|
|
|
185
|
-
// [PROGRESS LOG] This should now fire correctly
|
|
186
199
|
logger.log('INFO', `[Dispatcher] Analyzed ${d}: ${validToRun.length} tasks (Cumulative: ${tasksToDispatch.length})`);
|
|
187
200
|
|
|
188
201
|
} catch (e) {
|
|
@@ -196,14 +209,13 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
196
209
|
|
|
197
210
|
if (tasksToDispatch.length > 0) {
|
|
198
211
|
|
|
199
|
-
// 1. Initialize Shared State Document (The Counter)
|
|
200
212
|
const runId = crypto.randomUUID();
|
|
201
213
|
const metaStatePath = `computation_runs/${runId}`;
|
|
202
214
|
|
|
203
215
|
if (callbackUrl) {
|
|
204
216
|
await db.doc(metaStatePath).set({
|
|
205
217
|
createdAt: new Date(),
|
|
206
|
-
date: dateStr,
|
|
218
|
+
date: dateStr,
|
|
207
219
|
pass: passToRun,
|
|
208
220
|
totalTasks: tasksToDispatch.length,
|
|
209
221
|
remainingTasks: tasksToDispatch.length,
|
|
@@ -213,29 +225,23 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
213
225
|
logger.log('INFO', `[Dispatcher] 🏁 Run State Initialized: ${runId}. Tasks: ${tasksToDispatch.length}`);
|
|
214
226
|
}
|
|
215
227
|
|
|
216
|
-
// 2. Attach Run Metadata
|
|
217
228
|
tasksToDispatch.forEach(task => {
|
|
218
229
|
task.runId = runId;
|
|
219
230
|
task.metaStatePath = callbackUrl ? metaStatePath : null;
|
|
220
231
|
});
|
|
221
232
|
|
|
222
|
-
// 3. Create Audit Ledger Entries
|
|
223
233
|
const finalDispatched = [];
|
|
224
|
-
const txnLimit = pLimit(50);
|
|
234
|
+
const txnLimit = pLimit(50);
|
|
225
235
|
|
|
226
236
|
const txnPromises = tasksToDispatch.map(task => txnLimit(async () => {
|
|
227
237
|
const ledgerRef = db.collection(`computation_audit_ledger/${task.date}/passes/${task.pass}/tasks`).doc(task.computation);
|
|
228
|
-
|
|
229
238
|
try {
|
|
230
239
|
await db.runTransaction(async (t) => {
|
|
231
240
|
const doc = await t.get(ledgerRef);
|
|
232
|
-
|
|
233
241
|
if (doc.exists) {
|
|
234
242
|
const data = doc.data();
|
|
235
|
-
// Strict Idempotency: If completed, don't run again.
|
|
236
243
|
if (data.status === 'COMPLETED') return false;
|
|
237
244
|
}
|
|
238
|
-
|
|
239
245
|
t.set(ledgerRef, {
|
|
240
246
|
status: 'PENDING',
|
|
241
247
|
dispatchId: task.dispatchId,
|
|
@@ -245,15 +251,12 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
245
251
|
createdAt: new Date(),
|
|
246
252
|
dispatcherHash: currentManifestHash,
|
|
247
253
|
triggerReason: task.triggerReason,
|
|
248
|
-
resources: task.resources,
|
|
254
|
+
resources: task.resources,
|
|
249
255
|
retries: 0
|
|
250
256
|
}, { merge: true });
|
|
251
|
-
|
|
252
257
|
return true;
|
|
253
258
|
});
|
|
254
|
-
|
|
255
259
|
finalDispatched.push(task);
|
|
256
|
-
|
|
257
260
|
} catch (txnErr) {
|
|
258
261
|
logger.log('WARN', `[Dispatcher] Transaction failed for ${task.computation}: ${txnErr.message}`);
|
|
259
262
|
}
|
|
@@ -261,13 +264,10 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
261
264
|
|
|
262
265
|
await Promise.all(txnPromises);
|
|
263
266
|
|
|
264
|
-
// 4. Publish to Pub/Sub (Segregated by Resources)
|
|
265
267
|
if (finalDispatched.length > 0) {
|
|
266
|
-
|
|
267
268
|
const standardTasks = finalDispatched.filter(t => t.resources !== 'high-mem');
|
|
268
269
|
const highMemTasks = finalDispatched.filter(t => t.resources === 'high-mem');
|
|
269
270
|
|
|
270
|
-
// Publish Standard
|
|
271
271
|
if (standardTasks.length > 0) {
|
|
272
272
|
logger.log('INFO', `[Dispatcher] ✅ Publishing ${standardTasks.length} Standard tasks...`);
|
|
273
273
|
await pubsubUtils.batchPublishTasks(dependencies, {
|
|
@@ -278,7 +278,6 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
278
278
|
});
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
// Publish High-Mem
|
|
282
281
|
if (highMemTasks.length > 0) {
|
|
283
282
|
logger.log('INFO', `[Dispatcher] 🏋️♀️ Publishing ${highMemTasks.length} tasks to HIGH-MEM infrastructure.`);
|
|
284
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
|