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 (
|
|
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
|
-
//
|
|
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,
|
|
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)
|
|
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;
|
|
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) {
|
|
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';
|
|
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
|
-
|
|
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 => {
|
|
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
|
}
|