bulltrackers-module 1.0.325 → 1.0.326

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.
@@ -2,6 +2,7 @@
2
2
  * FILENAME: computation-system/helpers/computation_dispatcher.js
3
3
  * PURPOSE: Sequential Cursor-Based Dispatcher.
4
4
  * BEHAVIOR: Dispatch -> Wait ETA -> Next Date.
5
+ * UPDATED: Added "Zombie Protocol" to auto-recover stale locks.
5
6
  */
6
7
 
7
8
  const { getExpectedDateStrings, getEarliestDataDates, normalizeName, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils.js');
@@ -15,10 +16,14 @@ const OOM_THRESHOLD_MB = 1500;
15
16
  const BASE_SECONDS_PER_WEIGHT_UNIT = 3;
16
17
  const SESSION_CACHE_DURATION_MS = 1000 * 60 * 30; // 30 Minutes
17
18
 
19
+ // [NEW] Zombie Timeout: Max Cloud Function run is 9m (540s).
20
+ // If no heartbeat/start within 15m, it's definitely dead.
21
+ const STALE_LOCK_THRESHOLD_MS = 1000 * 60 * 15;
22
+
18
23
  // =============================================================================
19
- // HELPER: Ledger Awareness (Prevents Race Conditions)
24
+ // HELPER: Ledger Awareness (Prevents Race Conditions & Clears Zombies)
20
25
  // =============================================================================
21
- async function filterActiveTasks(db, date, pass, tasks) {
26
+ async function filterActiveTasks(db, date, pass, tasks, logger) {
22
27
  if (!tasks || tasks.length === 0) return [];
23
28
 
24
29
  const checkPromises = tasks.map(async (t) => {
@@ -28,13 +33,37 @@ async function filterActiveTasks(db, date, pass, tasks) {
28
33
 
29
34
  if (snap.exists) {
30
35
  const data = snap.data();
31
- // Check PENDING, IN_PROGRESS, or "Ghost" (Completed < 1 min ago)
32
36
  const isActive = ['PENDING', 'IN_PROGRESS'].includes(data.status);
37
+
38
+ // 1. ZOMBIE CHECK (Recover Stale Locks)
39
+ if (isActive) {
40
+ // Prefer heartbeat, fall back to start time
41
+ const lastActivityTime = data.telemetry?.lastHeartbeat
42
+ ? new Date(data.telemetry.lastHeartbeat).getTime()
43
+ : (data.startedAt ? new Date(data.startedAt).getTime() : 0);
44
+
45
+ const timeSinceActive = Date.now() - lastActivityTime;
46
+
47
+ if (timeSinceActive > STALE_LOCK_THRESHOLD_MS) {
48
+ if (logger) {
49
+ logger.log('WARN', `[Dispatcher] 🧟 Breaking stale lock for ${taskName}. Inactive for ${(timeSinceActive/60000).toFixed(1)} mins.`);
50
+ }
51
+ // Return task (Re-dispatching it will overwrite the old lock in Firestore)
52
+ return t;
53
+ }
54
+
55
+ // If distinct and recent, filter it out (let it run)
56
+ return null;
57
+ }
58
+
59
+ // 2. GHOST CHECK (Debounce immediate re-runs)
60
+ // If it finished less than 1 minute ago, don't re-dispatch immediately
61
+ // (prevents double-tap if latency is high)
33
62
  const isJustFinished = data.status === 'COMPLETED' &&
34
63
  data.completedAt &&
35
64
  (Date.now() - new Date(data.completedAt).getTime() < 60 * 1000);
36
65
 
37
- if (isActive || isJustFinished) return null; // Filter out
66
+ if (isJustFinished) return null; // Filter out
38
67
  }
39
68
  return t;
40
69
  });
@@ -110,7 +139,6 @@ async function getStableDateSession(config, dependencies, passToRun, dateLimitSt
110
139
  if (sessionSnap.exists) {
111
140
  const data = sessionSnap.data();
112
141
  if ((Date.now() - new Date(data.createdAt).getTime()) < SESSION_CACHE_DURATION_MS) {
113
- // logger.log('INFO', `[Session] 📂 Loaded stable session for Pass ${passToRun}.`);
114
142
  return data.dates;
115
143
  }
116
144
  }
@@ -184,8 +212,8 @@ async function dispatchComputationPass(config, dependencies, computationManifest
184
212
  if (rawTasks.length > 0) {
185
213
  rawTasks = await attemptSimHashResolution(dependencies, selectedDate, rawTasks, dailyStatus, manifestMap);
186
214
 
187
- // Ledger Filter: Removes tasks that are already running
188
- selectedTasks = await filterActiveTasks(db, selectedDate, passToRun, rawTasks);
215
+ // [UPDATED] Pass logger to filterActiveTasks for zombie warnings
216
+ selectedTasks = await filterActiveTasks(db, selectedDate, passToRun, rawTasks, logger);
189
217
  }
190
218
 
191
219
  // OOM / High-Mem Reroute Check
@@ -200,7 +228,6 @@ async function dispatchComputationPass(config, dependencies, computationManifest
200
228
 
201
229
  // 4. Dispatch Logic
202
230
  if (selectedTasks.length === 0) {
203
- // Return 0 dispatched, FALSE cursor ignored -> Move to NEXT date immediately.
204
231
  return {
205
232
  status: 'CONTINUE_PASS',
206
233
  dateProcessed: selectedDate,
@@ -258,14 +285,11 @@ async function dispatchComputationPass(config, dependencies, computationManifest
258
285
  }
259
286
  await Promise.all(pubPromises);
260
287
 
261
- // CRITICAL: We dispatched work.
262
- // We return n_cursor_ignored: FALSE.
263
- // This tells the workflow to Wait ETA -> Increment Cursor -> Move to Next Date.
264
288
  return {
265
289
  status: 'CONTINUE_PASS',
266
290
  dateProcessed: selectedDate,
267
291
  dispatched: selectedTasks.length,
268
- n_cursor_ignored: false, // FORCE NEXT DATE
292
+ n_cursor_ignored: false,
269
293
  etaSeconds: etaSeconds,
270
294
  remainingDates: sessionDates.length - targetCursorN
271
295
  };
@@ -2,6 +2,7 @@
2
2
  * FILENAME: bulltrackers-module/functions/computation-system/tools/BuildReporter.js
3
3
  * UPGRADED: Offloads heavy logic to a dedicated Cloud Function via Pub/Sub.
4
4
  * FEATURES: Patch versioning, data-drift detection (window changes), and checkpointed writes.
5
+ * FIX: Ensures ALL dates in the window are reported, even if analysis fails.
5
6
  */
6
7
 
7
8
  const { analyzeDateExecution } = require('../WorkflowOrchestrator');
@@ -62,7 +63,7 @@ async function handleBuildReportTrigger(message, context, config, dependencies,
62
63
  function getSystemFingerprint(manifest) {
63
64
  const sortedManifestHashes = manifest.map(c => c.hash).sort().join('|');
64
65
  return crypto.createHash('sha256')
65
- .update(sortedManifestHashes + SYSTEM_EPOCH + REPORTER_EPOCH) // [UPDATED]
66
+ .update(sortedManifestHashes + SYSTEM_EPOCH + REPORTER_EPOCH)
66
67
  .digest('hex');
67
68
  }
68
69
 
@@ -187,20 +188,20 @@ async function generateBuildReport(config, dependencies, manifest) {
187
188
  const lastEarliestStr = latest?.windowEarliest || 'NONE';
188
189
  const windowChanged = currentEarliestStr !== lastEarliestStr;
189
190
 
190
- const epochChanged = latest?.reporterEpoch !== REPORTER_EPOCH; // [NEW]
191
+ const epochChanged = latest?.reporterEpoch !== REPORTER_EPOCH;
191
192
 
192
193
  // If fingerprints match AND the window is the same, we can truly skip.
193
194
  if (latest &&
194
195
  latest.systemFingerprint === currentFingerprint &&
195
196
  !windowChanged &&
196
- !epochChanged) { // [NEW]
197
+ !epochChanged) {
197
198
  logger.log('INFO', `[BuildReporter] ⚡ System fingerprint, window, and reporter epoch stable. Skipping report.`);
198
199
  return { success: true, status: 'SKIPPED_IDENTICAL' };
199
200
  }
200
201
 
201
202
  // Determine primary reason for logging
202
203
  let reason = 'Code Change';
203
- if (epochChanged) reason = 'Master Epoch Override'; // [NEW]
204
+ if (epochChanged) reason = 'Master Epoch Override';
204
205
  else if (windowChanged) reason = 'Data Window Drift';
205
206
 
206
207
  // Increment patch version
@@ -251,7 +252,7 @@ async function generateBuildReport(config, dependencies, manifest) {
251
252
  // Initialize the build record
252
253
  await db.collection(BUILD_RECORDS_COLLECTION).doc(buildId).set(reportHeader);
253
254
 
254
- let totalRun = 0, totalReRun = 0, totalStable = 0;
255
+ let totalRun = 0, totalReRun = 0, totalStable = 0, totalErrors = 0;
255
256
  const limit = pLimit(10); // Concurrency for fetching statuses
256
257
 
257
258
  // Process dates in chunks of 5 for checkpointed writing
@@ -336,17 +337,33 @@ async function generateBuildReport(config, dependencies, manifest) {
336
337
  // Write detailed date record
337
338
  await db.collection(BUILD_RECORDS_COLLECTION).doc(buildId).collection('details').doc(dateStr).set(dateSummary);
338
339
 
339
- return { run: dateSummary.run.length, rerun: dateSummary.rerun.length, stable: dateSummary.stable.length };
340
+ return { run: dateSummary.run.length, rerun: dateSummary.rerun.length, stable: dateSummary.stable.length, error: false };
340
341
  } catch (err) {
341
342
  logger.log('ERROR', `[BuildReporter] Analysis failed for ${dateStr}: ${err.message}`);
342
- return { run: 0, rerun: 0, stable: 0 };
343
+
344
+ // [FIX] Write error record so the date appears in the report
345
+ await db.collection(BUILD_RECORDS_COLLECTION).doc(buildId).collection('details').doc(dateStr).set({
346
+ error: err.message,
347
+ status: 'ANALYSIS_FAILED',
348
+ meta: { totalIncluded: 0, totalExpected: 0, match: false }
349
+ }).catch(e => logger.log('ERROR', `Failed to write error record for ${dateStr}: ${e.message}`));
350
+
351
+ return { run: 0, rerun: 0, stable: 0, error: true };
343
352
  }
344
353
  })));
345
354
 
346
355
  // Accumulate stats and write a progress checkpoint
347
- results.forEach(res => { totalRun += res.run; totalReRun += res.rerun; totalStable += res.stable; });
356
+ results.forEach(res => {
357
+ if (res.error) totalErrors++;
358
+ else {
359
+ totalRun += res.run;
360
+ totalReRun += res.rerun;
361
+ totalStable += res.stable;
362
+ }
363
+ });
364
+
348
365
  await db.collection(BUILD_RECORDS_COLLECTION).doc(buildId).update({
349
- checkpoint: `Processed ${i + dateBatch.length}/${datesToCheck.length} dates`
366
+ checkpoint: `Processed ${Math.min(i + dateBatch.length, datesToCheck.length)}/${datesToCheck.length} dates`
350
367
  });
351
368
  }
352
369
 
@@ -355,6 +372,7 @@ async function generateBuildReport(config, dependencies, manifest) {
355
372
  totalReRuns: totalReRun,
356
373
  totalNew: totalRun,
357
374
  totalStable: totalStable,
375
+ totalErrors: totalErrors,
358
376
  scanRange: `${datesToCheck[0]} to ${datesToCheck[datesToCheck.length-1]}`
359
377
  };
360
378
 
@@ -362,7 +380,7 @@ async function generateBuildReport(config, dependencies, manifest) {
362
380
  await db.collection(BUILD_RECORDS_COLLECTION).doc(buildId).set(reportHeader);
363
381
  await db.collection(BUILD_RECORDS_COLLECTION).doc('latest').set({ ...reportHeader, note: "Latest completed build report." });
364
382
 
365
- logger.log('SUCCESS', `[BuildReporter] Build ${buildId} completed. Re-runs: ${totalReRun}, Stable: ${totalStable}, New: ${totalRun}.`);
383
+ logger.log('SUCCESS', `[BuildReporter] Build ${buildId} completed. Re-runs: ${totalReRun}, Stable: ${totalStable}, New: ${totalRun}, Errors: ${totalErrors}.`);
366
384
 
367
385
  return { success: true, buildId, summary: reportHeader.summary };
368
386
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.325",
3
+ "version": "1.0.326",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [