bulltrackers-module 1.0.273 → 1.0.275
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 and only dispatches valid, runnable tasks.
|
|
4
|
-
* UPDATED:
|
|
4
|
+
* UPDATED: Adds 'dispatchId' to payloads for precise tracing.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { getExpectedDateStrings, normalizeName, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils.js');
|
|
@@ -11,13 +11,13 @@ const { fetchComputationStatus, updateComputationStatus } = require('../persiste
|
|
|
11
11
|
const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
|
|
12
12
|
const { generateCodeHash } = require('../topology/HashManager');
|
|
13
13
|
const pLimit = require('p-limit');
|
|
14
|
+
const crypto = require('crypto'); // REQUIRED for UUID
|
|
14
15
|
|
|
15
16
|
const TOPIC_NAME = 'computation-tasks';
|
|
16
17
|
const STATUS_IMPOSSIBLE = 'IMPOSSIBLE';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Dispatches computation tasks for a specific pass.
|
|
20
|
-
* Performs full pre-flight checks (Root Data, Dependencies, History) before emitting.
|
|
21
21
|
*/
|
|
22
22
|
async function dispatchComputationPass(config, dependencies, computationManifest) {
|
|
23
23
|
const { logger, db } = dependencies;
|
|
@@ -26,43 +26,36 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
26
26
|
|
|
27
27
|
if (!passToRun) { return logger.log('ERROR', '[Dispatcher] No pass defined (COMPUTATION_PASS_TO_RUN). Aborting.'); }
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
const currentManifestHash = generateCodeHash(
|
|
30
|
+
computationManifest.map(c => c.hash).sort().join('|')
|
|
31
|
+
);
|
|
32
|
+
|
|
30
33
|
const passes = groupByPass(computationManifest);
|
|
31
34
|
const calcsInThisPass = passes[passToRun] || [];
|
|
32
35
|
|
|
33
36
|
if (!calcsInThisPass.length) { return logger.log('WARN', `[Dispatcher] No calcs for Pass ${passToRun}. Exiting.`); }
|
|
34
37
|
|
|
35
|
-
// --- [NEW] OPTIMIZATION 1: PREEMPTIVE HASH CHECK ---
|
|
36
|
-
const currentManifestHash = generateCodeHash(
|
|
37
|
-
computationManifest.map(c => c.hash).sort().join('|')
|
|
38
|
-
);
|
|
39
|
-
// ---------------------------------------------------
|
|
40
|
-
|
|
41
38
|
const calcNames = calcsInThisPass.map(c => c.name);
|
|
42
39
|
logger.log('INFO', `🚀 [Dispatcher] Smart-Dispatching PASS ${passToRun}`);
|
|
43
40
|
logger.log('INFO', `[Dispatcher] Target Calculations: [${calcNames.join(', ')}]`);
|
|
44
41
|
|
|
45
|
-
// 2. Determine Date Range
|
|
46
42
|
const passEarliestDate = Object.values(DEFINITIVE_EARLIEST_DATES).reduce((a, b) => a < b ? a : b);
|
|
47
43
|
const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
|
|
48
44
|
const allExpectedDates = getExpectedDateStrings(passEarliestDate, endDateUTC);
|
|
49
45
|
|
|
50
46
|
const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
|
|
51
47
|
const tasksToDispatch = [];
|
|
52
|
-
const limit = pLimit(20);
|
|
48
|
+
const limit = pLimit(20);
|
|
53
49
|
|
|
54
50
|
logger.log('INFO', `[Dispatcher] Analyzing ${allExpectedDates.length} dates for viability...`);
|
|
55
51
|
|
|
56
|
-
// 3. Analyze Each Date (Concurrent)
|
|
57
52
|
const analysisPromises = allExpectedDates.map(dateStr => limit(async () => {
|
|
58
53
|
try {
|
|
59
|
-
// [NEW] OPTIMIZATION 3: PARALLEL STATUS FETCH
|
|
60
54
|
const fetchPromises = [
|
|
61
|
-
fetchComputationStatus(dateStr, config, dependencies),
|
|
62
|
-
checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES)
|
|
55
|
+
fetchComputationStatus(dateStr, config, dependencies),
|
|
56
|
+
checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES)
|
|
63
57
|
];
|
|
64
58
|
|
|
65
|
-
// B. Fetch Status (Yesterday) - Only if historical continuity is needed
|
|
66
59
|
let prevDateStr = null;
|
|
67
60
|
if (calcsInThisPass.some(c => c.isHistorical)) {
|
|
68
61
|
const prevDate = new Date(dateStr + 'T00:00:00Z');
|
|
@@ -83,25 +76,20 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
83
76
|
hasPortfolio: false, hasHistory: false, hasSocial: false, hasInsights: false, hasPrices: false
|
|
84
77
|
};
|
|
85
78
|
|
|
86
|
-
// D. Run Core Analysis Logic
|
|
87
79
|
const report = analyzeDateExecution(dateStr, calcsInThisPass, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus);
|
|
88
80
|
|
|
89
|
-
// E. Handle Non-Runnable States (Write directly to DB, don't dispatch)
|
|
90
81
|
const statusUpdates = {};
|
|
91
82
|
|
|
92
|
-
// Mark Impossible (Permanent Failure)
|
|
93
83
|
report.impossible.forEach(item => {
|
|
94
84
|
if (dailyStatus[item.name]?.hash !== STATUS_IMPOSSIBLE) {
|
|
95
85
|
statusUpdates[item.name] = { hash: STATUS_IMPOSSIBLE, category: 'unknown', reason: item.reason };
|
|
96
86
|
}
|
|
97
87
|
});
|
|
98
88
|
|
|
99
|
-
// Mark Blocked (Explicit Block)
|
|
100
89
|
report.blocked.forEach(item => {
|
|
101
90
|
statusUpdates[item.name] = { hash: false, category: 'unknown', reason: item.reason };
|
|
102
91
|
});
|
|
103
92
|
|
|
104
|
-
// [FIX] Mark Failed Dependencies (Implicit Block) - Safely generate reason string
|
|
105
93
|
report.failedDependency.forEach(item => {
|
|
106
94
|
const missingStr = item.missing ? item.missing.join(', ') : 'unknown';
|
|
107
95
|
statusUpdates[item.name] = {
|
|
@@ -115,11 +103,14 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
115
103
|
await updateComputationStatus(dateStr, statusUpdates, config, dependencies);
|
|
116
104
|
}
|
|
117
105
|
|
|
118
|
-
// F. Queue Runnables
|
|
119
106
|
const validToRun = [...report.runnable, ...report.reRuns];
|
|
120
107
|
validToRun.forEach(item => {
|
|
108
|
+
// [NEW] Generate Unique ID
|
|
109
|
+
const uniqueDispatchId = crypto.randomUUID();
|
|
110
|
+
|
|
121
111
|
tasksToDispatch.push({
|
|
122
112
|
action: 'RUN_COMPUTATION_DATE',
|
|
113
|
+
dispatchId: uniqueDispatchId, // <--- TRACKING ID
|
|
123
114
|
date: dateStr,
|
|
124
115
|
pass: passToRun,
|
|
125
116
|
computation: normalizeName(item.name),
|
|
@@ -137,13 +128,11 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
137
128
|
|
|
138
129
|
await Promise.all(analysisPromises);
|
|
139
130
|
|
|
140
|
-
// 4. Dispatch Valid Tasks with Atomic Ledger Check
|
|
141
131
|
if (tasksToDispatch.length > 0) {
|
|
142
132
|
logger.log('INFO', `[Dispatcher] 📝 Creating Audit Ledger entries (Transactional) for ${tasksToDispatch.length} tasks...`);
|
|
143
133
|
|
|
144
|
-
// --- [NEW] OPTIMIZATION 2: ATOMIC TRANSACTION FOR LEDGER ---
|
|
145
134
|
const finalDispatched = [];
|
|
146
|
-
const txnLimit = pLimit(20);
|
|
135
|
+
const txnLimit = pLimit(20);
|
|
147
136
|
|
|
148
137
|
const txnPromises = tasksToDispatch.map(task => txnLimit(async () => {
|
|
149
138
|
const ledgerRef = db.collection(`computation_audit_ledger/${task.date}/passes/${task.pass}/tasks`).doc(task.computation);
|
|
@@ -151,23 +140,27 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
151
140
|
try {
|
|
152
141
|
await db.runTransaction(async (t) => {
|
|
153
142
|
const doc = await t.get(ledgerRef);
|
|
143
|
+
|
|
144
|
+
// If task is PENDING, we assume it's running.
|
|
145
|
+
// However, we now OVERWRITE if it's been pending for > 1 hour (stuck state)
|
|
146
|
+
// For safety on your budget, we stick to strict "PENDING" check.
|
|
154
147
|
if (doc.exists && doc.data().status === 'PENDING') {
|
|
155
|
-
// Task is already pending from another dispatcher, Skip.
|
|
156
148
|
return false;
|
|
157
149
|
}
|
|
150
|
+
|
|
158
151
|
t.set(ledgerRef, {
|
|
159
152
|
status: 'PENDING',
|
|
153
|
+
dispatchId: task.dispatchId, // <--- Store ID in Ledger
|
|
160
154
|
computation: task.computation,
|
|
161
155
|
expectedHash: task.hash || 'unknown',
|
|
162
156
|
createdAt: new Date(),
|
|
163
|
-
dispatcherHash: currentManifestHash,
|
|
164
|
-
triggerReason: task.triggerReason,
|
|
157
|
+
dispatcherHash: currentManifestHash,
|
|
158
|
+
triggerReason: task.triggerReason,
|
|
165
159
|
retries: 0
|
|
166
160
|
}, { merge: true });
|
|
167
161
|
return true;
|
|
168
162
|
});
|
|
169
163
|
|
|
170
|
-
// Only dispatch if we successfully reserved the PENDING state
|
|
171
164
|
finalDispatched.push(task);
|
|
172
165
|
|
|
173
166
|
} catch (txnErr) {
|
|
@@ -176,7 +169,6 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
176
169
|
}));
|
|
177
170
|
|
|
178
171
|
await Promise.all(txnPromises);
|
|
179
|
-
// ---------------------------------------------------
|
|
180
172
|
|
|
181
173
|
if (finalDispatched.length > 0) {
|
|
182
174
|
logger.log('INFO', `[Dispatcher] ✅ Publishing ${finalDispatched.length} unique tasks to Pub/Sub...`);
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FILENAME: computation-system/helpers/computation_worker.js
|
|
3
|
-
* PURPOSE: Consumes computation tasks from Pub/Sub
|
|
4
|
-
* UPDATED:
|
|
5
|
-
* UPDATED:
|
|
6
|
-
* UPDATED: Now logs the trigger reason.
|
|
3
|
+
* PURPOSE: Consumes computation tasks from Pub/Sub.
|
|
4
|
+
* UPDATED: Logs 'dispatchId' for tracing.
|
|
5
|
+
* UPDATED: Includes Deterministic Error Short-Circuit (Poison Pill Protection).
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
const { executeDispatchTask } = require('../WorkflowOrchestrator.js');
|
|
@@ -15,18 +14,14 @@ let calculationPackage;
|
|
|
15
14
|
try { calculationPackage = require('aiden-shared-calculations-unified');
|
|
16
15
|
} catch (e) {console.error("FATAL: Could not load 'aiden-shared-calculations-unified'."); throw e; }
|
|
17
16
|
const calculations = calculationPackage.calculations;
|
|
18
|
-
const MAX_RETRIES =
|
|
17
|
+
const MAX_RETRIES = 0; // <--- CHANGED TO 0 (Application level check, though Pub/Sub config is better)
|
|
19
18
|
|
|
20
|
-
/**
|
|
21
|
-
* Handles a single Pub/Sub message.
|
|
22
|
-
*/
|
|
23
19
|
async function handleComputationTask(message, config, dependencies) {
|
|
24
20
|
const systemLogger = new StructuredLogger({ minLevel: config.minLevel || 'INFO', enableStructured: true, ...config });
|
|
25
21
|
const runDependencies = { ...dependencies, logger: systemLogger };
|
|
26
22
|
const { logger, db } = runDependencies;
|
|
27
23
|
let data;
|
|
28
24
|
|
|
29
|
-
// ----------------------------------- Parse message -----------------------------------
|
|
30
25
|
try {
|
|
31
26
|
if (message.data && message.data.message && message.data.message.data) { data = JSON.parse(Buffer.from(message.data.message.data, 'base64').toString());
|
|
32
27
|
} else if (message.data && typeof message.data === 'string') { data = JSON.parse(Buffer.from(message.data, 'base64').toString());
|
|
@@ -34,24 +29,28 @@ async function handleComputationTask(message, config, dependencies) {
|
|
|
34
29
|
} else { data = message; }
|
|
35
30
|
} catch (parseError) { logger.log('ERROR', `[Worker] Failed to parse Pub/Sub payload.`, { error: parseError.message }); return; }
|
|
36
31
|
|
|
37
|
-
// ----------------------------------- Validate & Execute -----------------------------------
|
|
38
32
|
if (!data || data.action !== 'RUN_COMPUTATION_DATE') { return; }
|
|
39
33
|
|
|
40
|
-
// Extract Trigger Reason
|
|
41
|
-
const { date, pass, computation, previousCategory, triggerReason } = data;
|
|
34
|
+
// Extract Trigger Reason and Dispatch ID
|
|
35
|
+
const { date, pass, computation, previousCategory, triggerReason, dispatchId } = data;
|
|
42
36
|
|
|
43
|
-
if (!date || !pass || !computation) { logger.log('ERROR', `[Worker] Invalid payload
|
|
37
|
+
if (!date || !pass || !computation) { logger.log('ERROR', `[Worker] Invalid payload.`, data); return; }
|
|
38
|
+
|
|
39
|
+
// LOG THE ID FOR TRACING
|
|
40
|
+
logger.log('INFO', `[Worker] 📥 Received Task: ${computation} (${date})`, {
|
|
41
|
+
dispatchId: dispatchId || 'legacy',
|
|
42
|
+
reason: triggerReason
|
|
43
|
+
});
|
|
44
|
+
|
|
44
45
|
let computationManifest;
|
|
45
46
|
try { computationManifest = getManifest(config.activeProductLines || [], calculations, runDependencies);
|
|
46
|
-
} catch (manifestError) {
|
|
47
|
-
|
|
47
|
+
} catch (manifestError) {
|
|
48
|
+
logger.log('FATAL', `[Worker] Failed to load Manifest: ${manifestError.message}`);
|
|
48
49
|
await recordRunAttempt(db, { date, computation, pass }, 'CRASH', { message: manifestError.message, stage: 'MANIFEST_LOAD' }, { durationMs: 0 }, triggerReason);
|
|
49
50
|
return;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
try {
|
|
53
|
-
logger.log('INFO', `[Worker] 📥 Received: ${computation} for ${date} [Reason: ${triggerReason || 'Unknown'}]`);
|
|
54
|
-
|
|
55
54
|
const startTime = Date.now();
|
|
56
55
|
const result = await executeDispatchTask(
|
|
57
56
|
date,
|
|
@@ -70,7 +69,7 @@ async function handleComputationTask(message, config, dependencies) {
|
|
|
70
69
|
if (failureReport.length > 0) {
|
|
71
70
|
const failReason = failureReport[0];
|
|
72
71
|
logger.log('ERROR', `[Worker] ❌ Failed logic/storage for ${computation}`, failReason.error);
|
|
73
|
-
const metrics
|
|
72
|
+
const metrics = failReason.metrics || {};
|
|
74
73
|
metrics.durationMs = duration;
|
|
75
74
|
await recordRunAttempt(db, { date, computation, pass }, 'FAILURE', failReason.error, metrics, triggerReason);
|
|
76
75
|
throw new Error(failReason.error.message || 'Computation Logic Failed');
|
|
@@ -79,9 +78,7 @@ async function handleComputationTask(message, config, dependencies) {
|
|
|
79
78
|
const successData = successUpdates[computation];
|
|
80
79
|
const metrics = successData.metrics || {};
|
|
81
80
|
metrics.durationMs = duration;
|
|
82
|
-
|
|
83
|
-
logger.log('INFO', `[Worker] ✅ Stored: ${computation}. Processed: ${metrics.execution?.processedUsers || metrics.execution?.processedItems || '?'} items.`);
|
|
84
|
-
|
|
81
|
+
logger.log('INFO', `[Worker] ✅ Stored: ${computation}. ID: ${dispatchId}`);
|
|
85
82
|
await recordRunAttempt(db, { date, computation, pass }, 'SUCCESS', null, metrics, triggerReason);
|
|
86
83
|
}
|
|
87
84
|
else {
|
|
@@ -89,12 +86,36 @@ async function handleComputationTask(message, config, dependencies) {
|
|
|
89
86
|
await recordRunAttempt(db, { date, computation, pass }, 'SUCCESS', { message: 'Empty Result' }, { durationMs: duration }, triggerReason);
|
|
90
87
|
}
|
|
91
88
|
} catch (err) {
|
|
89
|
+
// --- DETERMINISTIC ERROR SHORT-CIRCUIT ---
|
|
90
|
+
const isDeterministicError = err.stage === 'SHARDING_LIMIT_EXCEEDED' ||
|
|
91
|
+
err.stage === 'QUALITY_CIRCUIT_BREAKER' ||
|
|
92
|
+
(err.message && (err.message.includes('INVALID_ARGUMENT') || err.message.includes('Transaction too big')));
|
|
93
|
+
|
|
94
|
+
if (isDeterministicError) {
|
|
95
|
+
logger.log('ERROR', `[Worker] 🛑 Permanent Failure (Limit Issue). Sending to DLQ immediately: ${dispatchId}`);
|
|
96
|
+
try {
|
|
97
|
+
await db.collection('computation_dead_letter_queue').add({
|
|
98
|
+
originalData: data,
|
|
99
|
+
dispatchId: dispatchId,
|
|
100
|
+
error: { message: err.message, stack: err.stack, stage: err.stage || 'UNKNOWN' },
|
|
101
|
+
finalAttemptAt: new Date(),
|
|
102
|
+
failureReason: 'PERMANENT_DETERMINISTIC_ERROR'
|
|
103
|
+
});
|
|
104
|
+
// Return success to Pub/Sub to STOP retries
|
|
105
|
+
await recordRunAttempt(db, { date, computation, pass }, 'FAILURE', { message: err.message, stage: err.stage || 'PERMANENT_FAIL' }, { durationMs: 0 }, triggerReason);
|
|
106
|
+
return;
|
|
107
|
+
} catch (dlqErr) { logger.log('FATAL', `[Worker] Failed to write to DLQ`, dlqErr); }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- STANDARD RETRY ---
|
|
92
111
|
const retryCount = message.deliveryAttempt || 0;
|
|
112
|
+
// NOTE: If you configure Pub/Sub Max Attempts = 1, this logic is redundant but safe.
|
|
93
113
|
if (retryCount >= MAX_RETRIES) {
|
|
94
|
-
logger.log('ERROR', `[Worker] ☠️ Task POISONED. Moved to DLQ: ${computation}
|
|
114
|
+
logger.log('ERROR', `[Worker] ☠️ Task POISONED. Moved to DLQ: ${computation}`);
|
|
95
115
|
try {
|
|
96
116
|
await db.collection('computation_dead_letter_queue').add({
|
|
97
117
|
originalData: data,
|
|
118
|
+
dispatchId: dispatchId,
|
|
98
119
|
error: { message: err.message, stack: err.stack },
|
|
99
120
|
finalAttemptAt: new Date(),
|
|
100
121
|
failureReason: 'MAX_RETRIES_EXCEEDED'
|
|
@@ -102,7 +123,8 @@ async function handleComputationTask(message, config, dependencies) {
|
|
|
102
123
|
return;
|
|
103
124
|
} catch (dlqErr) { logger.log('FATAL', `[Worker] Failed to write to DLQ`, dlqErr); }
|
|
104
125
|
}
|
|
105
|
-
|
|
126
|
+
|
|
127
|
+
logger.log('ERROR', `[Worker] ❌ Crash: ${computation}: ${err.message}`);
|
|
106
128
|
await recordRunAttempt(db, { date, computation, pass }, 'CRASH', { message: err.message, stack: err.stack, stage: 'SYSTEM_CRASH' }, { durationMs: 0 }, triggerReason);
|
|
107
129
|
throw err;
|
|
108
130
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Generates a "Pre-Flight" report of what the computation system WILL do.
|
|
4
4
|
* REFACTORED: Strict 5-category reporting with date-based exclusion logic.
|
|
5
5
|
* UPDATED: Added transactional locking to prevent duplicate reports on concurrent cold starts.
|
|
6
|
+
* UPDATED: Adds 'pass' number to detail records for better waterfall visibility.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
const { analyzeDateExecution } = require('../WorkflowOrchestrator');
|
|
@@ -164,12 +165,18 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
164
165
|
dateSummary.meta.totalExpected = expectedCount;
|
|
165
166
|
|
|
166
167
|
// Helper to push only if date is valid for this specific calc
|
|
168
|
+
// [UPDATED] Adds 'pass' number to the record
|
|
167
169
|
const pushIfValid = (targetArray, item, extraReason = null) => {
|
|
168
170
|
const calcManifest = manifestMap.get(item.name);
|
|
169
171
|
if (calcManifest && isDateBeforeAvailability(dateStr, calcManifest)) {
|
|
170
172
|
return; // EXCLUDED: Date is before data exists
|
|
171
173
|
}
|
|
172
|
-
|
|
174
|
+
|
|
175
|
+
targetArray.push({
|
|
176
|
+
name: item.name,
|
|
177
|
+
reason: item.reason || extraReason,
|
|
178
|
+
pass: calcManifest ? calcManifest.pass : '?'
|
|
179
|
+
});
|
|
173
180
|
};
|
|
174
181
|
|
|
175
182
|
// 1. RUN (New)
|