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: Added per-date logging to visualize progress during large backfills.
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 'date'
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: if range is invalid or empty, default to target date only
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
- // Concurrency limit for analysis & forensics (Parallelize the historical scan)
108
- const limit = pLimit(20);
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 => limit(async () => {
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
- // [NEW] Parallel Forensics Check
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
- // 1. Determine Resource Requirements
166
- const requiredResource = await checkCrashForensics(db, d, passToRun, compName);
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 // 'standard' or 'high-mem'
195
+ resources: requiredResource
181
196
  });
182
197
  })));
183
198
 
184
- // [UPDATED] Add Heartbeat Log
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, // Acts as the "Job Label" (target date)
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(20);
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, // Log intended resource type
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
- # FIXED: Restored 'passes' and 'max_retries' variables in init step.
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 (Restored)
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 # UPDATED: Reduced from 86400 (24h) to 10800 (3h) to detect crashes faster
100
+ timeout: 10800 # Reduced to 3h to fail faster if stalled
100
101
  result: callback_request
101
102
 
102
103
  # 5. PROCESS SIGNAL
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.299",
3
+ "version": "1.0.301",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [