bulltrackers-module 1.0.640 → 1.0.642
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.
|
@@ -18,7 +18,28 @@ const crypto = require('crypto');
|
|
|
18
18
|
const OOM_THRESHOLD_MB = 1500; // Unused
|
|
19
19
|
const BASE_SECONDS_PER_WEIGHT_UNIT = 3;
|
|
20
20
|
const SESSION_CACHE_DURATION_MS = 1000 * 60 * 30; // 30 Minutes
|
|
21
|
-
const STALE_LOCK_THRESHOLD_MS = 1000 * 60 * 15;
|
|
21
|
+
const STALE_LOCK_THRESHOLD_MS = 1000 * 60 * 15;
|
|
22
|
+
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// HELPER: Firestore Timestamp Conversion
|
|
25
|
+
// =============================================================================
|
|
26
|
+
/**
|
|
27
|
+
* Converts a Firestore Timestamp or Date to milliseconds.
|
|
28
|
+
* Firestore stores Date objects as Timestamp objects, which have a .toDate() method.
|
|
29
|
+
* This function handles both cases correctly.
|
|
30
|
+
* @param {any} field - Firestore Timestamp, Date object, or string
|
|
31
|
+
* @returns {number} Milliseconds since epoch, or 0 if invalid
|
|
32
|
+
*/
|
|
33
|
+
function getMillis(field) {
|
|
34
|
+
if (!field) return 0;
|
|
35
|
+
// Handle Firestore Timestamp (has .toDate() method)
|
|
36
|
+
if (field.toDate && typeof field.toDate === 'function') {
|
|
37
|
+
return field.toDate().getTime();
|
|
38
|
+
}
|
|
39
|
+
// Handle standard Date object or string
|
|
40
|
+
const date = new Date(field);
|
|
41
|
+
return isNaN(date.getTime()) ? 0 : date.getTime();
|
|
42
|
+
}
|
|
22
43
|
|
|
23
44
|
// =============================================================================
|
|
24
45
|
// HELPER: Schedule Logic
|
|
@@ -73,9 +94,7 @@ async function filterActiveTasks(db, date, pass, tasks, logger, forceRun = false
|
|
|
73
94
|
const isActive = ['PENDING', 'IN_PROGRESS'].includes(data.status);
|
|
74
95
|
|
|
75
96
|
if (isActive) {
|
|
76
|
-
const lastActivityTime = data.telemetry?.lastHeartbeat
|
|
77
|
-
? new Date(data.telemetry.lastHeartbeat).getTime()
|
|
78
|
-
: (data.startedAt ? new Date(data.startedAt).getTime() : 0);
|
|
97
|
+
const lastActivityTime = getMillis(data.telemetry?.lastHeartbeat) || getMillis(data.startedAt);
|
|
79
98
|
|
|
80
99
|
if ((Date.now() - lastActivityTime) > STALE_LOCK_THRESHOLD_MS) {
|
|
81
100
|
if (logger) logger.log('WARN', `[Dispatcher] 🧟 Breaking stale lock for ${taskName}.`);
|
|
@@ -428,9 +447,7 @@ async function handleSweepDispatch(config, dependencies, computationManifest, re
|
|
|
428
447
|
|
|
429
448
|
// 1. ACTIVE CHECK: Don't double-dispatch if already running... UNLESS IT'S A ZOMBIE
|
|
430
449
|
if (['PENDING', 'IN_PROGRESS'].includes(data.status)) {
|
|
431
|
-
const lastActivity = data.telemetry?.lastHeartbeat
|
|
432
|
-
? new Date(data.telemetry.lastHeartbeat).getTime()
|
|
433
|
-
: (data.startedAt ? new Date(data.startedAt).getTime() : 0);
|
|
450
|
+
const lastActivity = getMillis(data.telemetry?.lastHeartbeat) || getMillis(data.startedAt);
|
|
434
451
|
|
|
435
452
|
// If it's been silent for > 15 mins, it's a Zombie. Kill it and Re-run.
|
|
436
453
|
if ((Date.now() - lastActivity) > STALE_LOCK_THRESHOLD_MS) {
|
|
@@ -566,6 +583,10 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
|
|
|
566
583
|
|
|
567
584
|
// Optimization: If nothing is scheduled for today, skip expensive DB checks
|
|
568
585
|
if (scheduledComputations.length === 0) {
|
|
586
|
+
// DEBUG: Log when schedule filtering removes all tasks
|
|
587
|
+
if (calcsInThisPass.length > 0) {
|
|
588
|
+
logger.log('TRACE', `[Dispatcher] Date ${selectedDate}: ${calcsInThisPass.length} pass computations, but 0 scheduled for this date. Skipping.`);
|
|
589
|
+
}
|
|
569
590
|
currentCursor++;
|
|
570
591
|
continue;
|
|
571
592
|
}
|
|
@@ -587,6 +608,11 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
|
|
|
587
608
|
checkRootDataAvailability(selectedDate, config, dependencies, DEFINITIVE_EARLIEST_DATES)
|
|
588
609
|
]);
|
|
589
610
|
|
|
611
|
+
// DEBUG: Log availability check
|
|
612
|
+
if (!availability || !availability.status) {
|
|
613
|
+
logger.log('WARN', `[Dispatcher] ⚠️ Date ${selectedDate}: Availability check failed or returned null. Skipping analysis.`);
|
|
614
|
+
}
|
|
615
|
+
|
|
590
616
|
if (availability && availability.status) {
|
|
591
617
|
const report = analyzeDateExecution(
|
|
592
618
|
selectedDate,
|
|
@@ -597,19 +623,37 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
|
|
|
597
623
|
prevDailyStatus
|
|
598
624
|
);
|
|
599
625
|
let rawTasks = [...report.runnable, ...report.reRuns];
|
|
626
|
+
|
|
627
|
+
// DEBUG: Log analysis results
|
|
628
|
+
if (rawTasks.length === 0 && (report.runnable.length > 0 || report.reRuns.length > 0)) {
|
|
629
|
+
logger.log('WARN', `[Dispatcher] ⚠️ Date ${selectedDate}: analyzeDateExecution found ${report.runnable.length} runnable + ${report.reRuns.length} reRuns, but rawTasks is empty!`);
|
|
630
|
+
}
|
|
631
|
+
if (rawTasks.length > 0) {
|
|
632
|
+
logger.log('TRACE', `[Dispatcher] Date ${selectedDate}: analyzeDateExecution found ${report.runnable.length} runnable, ${report.reRuns.length} reRuns. Total: ${rawTasks.length}`);
|
|
633
|
+
}
|
|
600
634
|
|
|
601
635
|
if (rawTasks.length > 0) {
|
|
602
636
|
rawTasks = await attemptSimHashResolution(dependencies, selectedDate, rawTasks, dailyStatus, manifestMap);
|
|
603
637
|
const activeTasks = await filterActiveTasks(db, selectedDate, passToRun, rawTasks, logger);
|
|
604
638
|
|
|
605
639
|
if (activeTasks.length > 0) {
|
|
640
|
+
// DEBUG: Log what we're about to route
|
|
641
|
+
logger.log('INFO', `[Dispatcher] 🔍 Date ${selectedDate}: ${rawTasks.length} raw tasks → ${activeTasks.length} after filtering. Routing...`);
|
|
606
642
|
const { standard, highMem } = await splitRoutes(db, selectedDate, passToRun, activeTasks, logger);
|
|
607
643
|
selectedTasks = [...standard, ...highMem];
|
|
608
644
|
|
|
645
|
+
// DEBUG: Log routing results
|
|
646
|
+
if (selectedTasks.length === 0 && activeTasks.length > 0) {
|
|
647
|
+
logger.log('WARN', `[Dispatcher] ⚠️ Date ${selectedDate}: ${activeTasks.length} tasks filtered out by splitRoutes! Tasks: ${activeTasks.map(t => t.name).join(', ')}`);
|
|
648
|
+
}
|
|
649
|
+
|
|
609
650
|
if (selectedTasks.length > 0) {
|
|
610
651
|
// Found work! Break loop to dispatch.
|
|
611
652
|
break;
|
|
612
653
|
}
|
|
654
|
+
} else if (rawTasks.length > 0) {
|
|
655
|
+
// DEBUG: Log if filterActiveTasks removed all tasks
|
|
656
|
+
logger.log('WARN', `[Dispatcher] ⚠️ Date ${selectedDate}: ${rawTasks.length} raw tasks all filtered out by filterActiveTasks! Tasks: ${rawTasks.map(t => t.name).join(', ')}`);
|
|
613
657
|
}
|
|
614
658
|
}
|
|
615
659
|
}
|
|
@@ -734,16 +778,31 @@ async function splitRoutes(db, date, pass, tasks, logger) {
|
|
|
734
778
|
const doc = await db.doc(ledgerPath).get();
|
|
735
779
|
|
|
736
780
|
if (!doc.exists) {
|
|
781
|
+
// No ledger entry - trust analyzeDateExecution, dispatch as standard
|
|
737
782
|
standard.push(task);
|
|
738
783
|
continue;
|
|
739
784
|
}
|
|
740
785
|
|
|
741
786
|
const data = doc.data();
|
|
742
787
|
|
|
743
|
-
//
|
|
788
|
+
// CRITICAL FIX: If analyzeDateExecution says this task should run, we MUST trust it.
|
|
789
|
+
// The ledger might say COMPLETED, but if computation_status is missing/outdated,
|
|
790
|
+
// we need to re-run to repair the state. Only skip if actively running.
|
|
791
|
+
// Note: filterActiveTasks already filtered out non-stale PENDING/IN_PROGRESS,
|
|
792
|
+
// but we double-check here in case of race conditions.
|
|
744
793
|
if (['PENDING', 'IN_PROGRESS'].includes(data.status)) {
|
|
745
|
-
//
|
|
746
|
-
|
|
794
|
+
// Check if it's stale (should have been caught by filterActiveTasks, but double-check)
|
|
795
|
+
const lastActivityTime = getMillis(data.telemetry?.lastHeartbeat) || getMillis(data.startedAt);
|
|
796
|
+
|
|
797
|
+
if ((Date.now() - lastActivityTime) > STALE_LOCK_THRESHOLD_MS) {
|
|
798
|
+
// Stale lock - break it and continue
|
|
799
|
+
logger.log('WARN', `[Dispatcher] 🧟 splitRoutes: Breaking stale lock for ${name}.`);
|
|
800
|
+
// Fall through to handle as if no active lock
|
|
801
|
+
} else {
|
|
802
|
+
// Valid active lock - skip (shouldn't happen if filterActiveTasks worked correctly)
|
|
803
|
+
logger.log('TRACE', `[Dispatcher] splitRoutes: Skipping ${name} - Valid IN_PROGRESS (should have been filtered earlier).`);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
747
806
|
}
|
|
748
807
|
|
|
749
808
|
if (data.status === 'FAILED') {
|
|
@@ -794,8 +853,12 @@ async function splitRoutes(db, date, pass, tasks, logger) {
|
|
|
794
853
|
});
|
|
795
854
|
|
|
796
855
|
} else {
|
|
797
|
-
// Status is likely COMPLETED
|
|
798
|
-
//
|
|
856
|
+
// Status is likely COMPLETED or some other state.
|
|
857
|
+
// CRITICAL: If analyzeDateExecution says this should run, we MUST trust it.
|
|
858
|
+
// The ledger might show COMPLETED, but if computation_status is missing/outdated,
|
|
859
|
+
// we need to re-run to repair the state. This is the "ghost state fix" logic.
|
|
860
|
+
// Trust the Brain (analyzeDateExecution) over the Log (ledger).
|
|
861
|
+
logger.log('INFO', `[Dispatcher] 🔄 splitRoutes: ${name} has ledger status '${data.status}', but analyzeDateExecution says it should run. Trusting analysis and dispatching.`);
|
|
799
862
|
standard.push(task);
|
|
800
863
|
}
|
|
801
864
|
}
|
|
@@ -399,35 +399,35 @@ async function generateBuildReport(config, dependencies, manifest) {
|
|
|
399
399
|
trueReRuns.forEach(item => processItem(dateSummary.rerun, item, "Logic Changed"));
|
|
400
400
|
stableUpdates.forEach(item => processItem(dateSummary.stable, item, "Logic Stable"));
|
|
401
401
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
402
|
+
// Auto-Heal Status if Stable
|
|
403
|
+
if (stableUpdates.length > 0) {
|
|
404
|
+
const updatesPayload = {};
|
|
405
|
+
for (const stable of stableUpdates) {
|
|
406
|
+
const m = manifestMap.get(stable.name);
|
|
407
|
+
const stored = dailyStatus[stable.name];
|
|
408
|
+
|
|
409
|
+
// [FIX] Only auto-heal if we have valid result data
|
|
410
|
+
// Otherwise, force re-run to regenerate everything properly
|
|
411
|
+
const hasValidResults = stored?.resultHash &&
|
|
412
|
+
stored.resultHash !== 'empty' &&
|
|
413
|
+
stored.dependencyResultHashes &&
|
|
414
|
+
Object.keys(stored.dependencyResultHashes).length > 0;
|
|
415
|
+
|
|
416
|
+
if (m && stored && hasValidResults) {
|
|
417
|
+
updatesPayload[stable.name] = {
|
|
418
|
+
hash: m.hash, simHash: stable.simHash, resultHash: stored.resultHash,
|
|
419
|
+
dependencyResultHashes: stored.dependencyResultHashes,
|
|
420
|
+
category: m.category, composition: m.composition, lastUpdated: new Date()
|
|
421
|
+
};
|
|
422
|
+
} else {
|
|
423
|
+
// No valid results - treat as true re-run
|
|
424
|
+
processItem(dateSummary.rerun, stable, "Epoch Change - Rebuilding");
|
|
425
|
+
}
|
|
425
426
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
}
|
|
427
|
+
if (Object.keys(updatesPayload).length > 0) {
|
|
428
|
+
await updateComputationStatus(dateStr, updatesPayload, config, dependencies);
|
|
429
|
+
}
|
|
430
|
+
}}
|
|
431
431
|
|
|
432
432
|
// Add skipped items to Stable count for metrics
|
|
433
433
|
analysis.skipped.forEach(item => processItem(dateSummary.stable, item, "Up To Date"));
|
|
@@ -176,7 +176,16 @@ class FinalSweepReporter {
|
|
|
176
176
|
forensics.ledgerState = ledgerState;
|
|
177
177
|
|
|
178
178
|
if (['PENDING', 'IN_PROGRESS'].includes(data.status)) {
|
|
179
|
-
|
|
179
|
+
// CRITICAL FIX: Handle Firestore Timestamp objects correctly
|
|
180
|
+
const getMillis = (field) => {
|
|
181
|
+
if (!field) return 0;
|
|
182
|
+
if (field.toDate && typeof field.toDate === 'function') {
|
|
183
|
+
return field.toDate().getTime();
|
|
184
|
+
}
|
|
185
|
+
const date = new Date(field);
|
|
186
|
+
return isNaN(date.getTime()) ? 0 : date.getTime();
|
|
187
|
+
};
|
|
188
|
+
const lastHb = getMillis(data.telemetry?.lastHeartbeat) || getMillis(data.startedAt);
|
|
180
189
|
if (Date.now() - lastHb > STALE_THRESHOLD_MS) {
|
|
181
190
|
forensics.rootCause = 'ZOMBIE_PROCESS';
|
|
182
191
|
forensics.reason = `Worker ${data.workerId} stopped heartbeating. Likely crashed/timeout.`;
|