bulltrackers-module 1.0.274 → 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: Fixed "undefined" reason crash for failed dependencies.
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
- // 1. Get Calculations for this Pass
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); // Process 20 days in parallel
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), // A. Current Status
62
- checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES) // C. Root Data
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); // Limit concurrent transactions
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, // Tracking source
164
- triggerReason: task.triggerReason, // Track trigger in ledger too
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,8 +1,8 @@
1
1
  /**
2
2
  * FILENAME: computation-system/helpers/computation_worker.js
3
- * PURPOSE: Consumes computation tasks from Pub/Sub and executes them.
4
- * UPDATED: Added Deterministic Error Short-Circuit to prevent infinite retry storms on data limits.
5
- * UPDATED: Integrated Run Ledger for per-run/per-date success/failure tracking.
3
+ * PURPOSE: Consumes computation tasks from Pub/Sub.
4
+ * UPDATED: Logs 'dispatchId' for tracing.
5
+ * UPDATED: Includes Deterministic Error Short-Circuit (Poison Pill Protection).
6
6
  */
7
7
 
8
8
  const { executeDispatchTask } = require('../WorkflowOrchestrator.js');
@@ -14,18 +14,14 @@ let calculationPackage;
14
14
  try { calculationPackage = require('aiden-shared-calculations-unified');
15
15
  } catch (e) {console.error("FATAL: Could not load 'aiden-shared-calculations-unified'."); throw e; }
16
16
  const calculations = calculationPackage.calculations;
17
- const MAX_RETRIES = 3;
17
+ const MAX_RETRIES = 0; // <--- CHANGED TO 0 (Application level check, though Pub/Sub config is better)
18
18
 
19
- /**
20
- * Handles a single Pub/Sub message.
21
- */
22
19
  async function handleComputationTask(message, config, dependencies) {
23
20
  const systemLogger = new StructuredLogger({ minLevel: config.minLevel || 'INFO', enableStructured: true, ...config });
24
21
  const runDependencies = { ...dependencies, logger: systemLogger };
25
22
  const { logger, db } = runDependencies;
26
23
  let data;
27
24
 
28
- // ----------------------------------- Parse message -----------------------------------
29
25
  try {
30
26
  if (message.data && message.data.message && message.data.message.data) { data = JSON.parse(Buffer.from(message.data.message.data, 'base64').toString());
31
27
  } else if (message.data && typeof message.data === 'string') { data = JSON.parse(Buffer.from(message.data, 'base64').toString());
@@ -33,24 +29,28 @@ async function handleComputationTask(message, config, dependencies) {
33
29
  } else { data = message; }
34
30
  } catch (parseError) { logger.log('ERROR', `[Worker] Failed to parse Pub/Sub payload.`, { error: parseError.message }); return; }
35
31
 
36
- // ----------------------------------- Validate & Execute -----------------------------------
37
32
  if (!data || data.action !== 'RUN_COMPUTATION_DATE') { return; }
38
33
 
39
- // Extract Trigger Reason
40
- const { date, pass, computation, previousCategory, triggerReason } = data;
34
+ // Extract Trigger Reason and Dispatch ID
35
+ const { date, pass, computation, previousCategory, triggerReason, dispatchId } = data;
41
36
 
42
- if (!date || !pass || !computation) { logger.log('ERROR', `[Worker] Invalid payload: Missing date, pass, or computation.`, data); return; }
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
+
43
45
  let computationManifest;
44
46
  try { computationManifest = getManifest(config.activeProductLines || [], calculations, runDependencies);
45
- } catch (manifestError) { logger.log('FATAL', `[Worker] Failed to load Manifest: ${manifestError.message}`);
46
- // FIX: Passing { durationMs: 0 } instead of {} to satisfy type requirements
47
+ } catch (manifestError) {
48
+ logger.log('FATAL', `[Worker] Failed to load Manifest: ${manifestError.message}`);
47
49
  await recordRunAttempt(db, { date, computation, pass }, 'CRASH', { message: manifestError.message, stage: 'MANIFEST_LOAD' }, { durationMs: 0 }, triggerReason);
48
50
  return;
49
51
  }
50
52
 
51
53
  try {
52
- logger.log('INFO', `[Worker] 📥 Received: ${computation} for ${date} [Reason: ${triggerReason || 'Unknown'}]`);
53
-
54
54
  const startTime = Date.now();
55
55
  const result = await executeDispatchTask(
56
56
  date,
@@ -69,7 +69,7 @@ async function handleComputationTask(message, config, dependencies) {
69
69
  if (failureReport.length > 0) {
70
70
  const failReason = failureReport[0];
71
71
  logger.log('ERROR', `[Worker] ❌ Failed logic/storage for ${computation}`, failReason.error);
72
- const metrics = failReason.metrics || {};
72
+ const metrics = failReason.metrics || {};
73
73
  metrics.durationMs = duration;
74
74
  await recordRunAttempt(db, { date, computation, pass }, 'FAILURE', failReason.error, metrics, triggerReason);
75
75
  throw new Error(failReason.error.message || 'Computation Logic Failed');
@@ -78,9 +78,7 @@ async function handleComputationTask(message, config, dependencies) {
78
78
  const successData = successUpdates[computation];
79
79
  const metrics = successData.metrics || {};
80
80
  metrics.durationMs = duration;
81
-
82
- logger.log('INFO', `[Worker] ✅ Stored: ${computation}. Processed: ${metrics.execution?.processedUsers || metrics.execution?.processedItems || '?'} items.`);
83
-
81
+ logger.log('INFO', `[Worker] ✅ Stored: ${computation}. ID: ${dispatchId}`);
84
82
  await recordRunAttempt(db, { date, computation, pass }, 'SUCCESS', null, metrics, triggerReason);
85
83
  }
86
84
  else {
@@ -88,41 +86,36 @@ async function handleComputationTask(message, config, dependencies) {
88
86
  await recordRunAttempt(db, { date, computation, pass }, 'SUCCESS', { message: 'Empty Result' }, { durationMs: duration }, triggerReason);
89
87
  }
90
88
  } catch (err) {
91
- // ----------------------------------- ERROR HANDLING & RETRY LOGIC -----------------------------------
92
-
93
- // 1. DETERMINISTIC ERROR CHECK (Short-Circuit)
94
- // If the error is permanent (like "Too Big" or "Validation Failed"), DO NOT RETRY.
95
- // This stops the "Retry Storm" where we pay for 3-4 retries of a task that will never succeed.
89
+ // --- DETERMINISTIC ERROR SHORT-CIRCUIT ---
96
90
  const isDeterministicError = err.stage === 'SHARDING_LIMIT_EXCEEDED' ||
97
91
  err.stage === 'QUALITY_CIRCUIT_BREAKER' ||
98
92
  (err.message && (err.message.includes('INVALID_ARGUMENT') || err.message.includes('Transaction too big')));
99
93
 
100
94
  if (isDeterministicError) {
101
- logger.log('ERROR', `[Worker] 🛑 Permanent Failure (Data/Limit Issue). Sending to DLQ immediately: ${computation} ${date}`);
95
+ logger.log('ERROR', `[Worker] 🛑 Permanent Failure (Limit Issue). Sending to DLQ immediately: ${dispatchId}`);
102
96
  try {
103
97
  await db.collection('computation_dead_letter_queue').add({
104
98
  originalData: data,
99
+ dispatchId: dispatchId,
105
100
  error: { message: err.message, stack: err.stack, stage: err.stage || 'UNKNOWN' },
106
101
  finalAttemptAt: new Date(),
107
102
  failureReason: 'PERMANENT_DETERMINISTIC_ERROR'
108
103
  });
109
-
110
- // CRITICAL: We record the failure but return successfully to Pub/Sub to ACK the message and stop retries.
111
- // This ensures the task is marked as Failed in run history, but does NOT block the queue.
104
+ // Return success to Pub/Sub to STOP retries
112
105
  await recordRunAttempt(db, { date, computation, pass }, 'FAILURE', { message: err.message, stage: err.stage || 'PERMANENT_FAIL' }, { durationMs: 0 }, triggerReason);
113
106
  return;
114
- } catch (dlqErr) {
115
- logger.log('FATAL', `[Worker] Failed to write to DLQ for deterministic error`, dlqErr);
116
- }
107
+ } catch (dlqErr) { logger.log('FATAL', `[Worker] Failed to write to DLQ`, dlqErr); }
117
108
  }
118
109
 
119
- // 2. STANDARD RETRY LOGIC (Timeout / Crash)
110
+ // --- STANDARD RETRY ---
120
111
  const retryCount = message.deliveryAttempt || 0;
112
+ // NOTE: If you configure Pub/Sub Max Attempts = 1, this logic is redundant but safe.
121
113
  if (retryCount >= MAX_RETRIES) {
122
- logger.log('ERROR', `[Worker] ☠️ Task POISONED. Moved to DLQ: ${computation} ${date} (Attempt ${retryCount})`);
114
+ logger.log('ERROR', `[Worker] ☠️ Task POISONED. Moved to DLQ: ${computation}`);
123
115
  try {
124
116
  await db.collection('computation_dead_letter_queue').add({
125
117
  originalData: data,
118
+ dispatchId: dispatchId,
126
119
  error: { message: err.message, stack: err.stack },
127
120
  finalAttemptAt: new Date(),
128
121
  failureReason: 'MAX_RETRIES_EXCEEDED'
@@ -131,8 +124,7 @@ async function handleComputationTask(message, config, dependencies) {
131
124
  } catch (dlqErr) { logger.log('FATAL', `[Worker] Failed to write to DLQ`, dlqErr); }
132
125
  }
133
126
 
134
- // If it's not deterministic and not max retries, we throw to let Pub/Sub retry it.
135
- logger.log('ERROR', `[Worker] ❌ Crash: ${computation} for ${date}: ${err.message}`);
127
+ logger.log('ERROR', `[Worker] Crash: ${computation}: ${err.message}`);
136
128
  await recordRunAttempt(db, { date, computation, pass }, 'CRASH', { message: err.message, stack: err.stack, stage: 'SYSTEM_CRASH' }, { durationMs: 0 }, triggerReason);
137
129
  throw err;
138
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
- targetArray.push({ name: item.name, reason: item.reason || extraReason });
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.274",
3
+ "version": "1.0.275",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [