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: 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,9 +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: Integrated Run Ledger for per-run/per-date success/failure tracking.
5
- * UPDATED: Added Dead Letter Queue logic for Poison Pills.
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 = 3;
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: 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
+
44
45
  let computationManifest;
45
46
  try { computationManifest = getManifest(config.activeProductLines || [], calculations, runDependencies);
46
- } catch (manifestError) { logger.log('FATAL', `[Worker] Failed to load Manifest: ${manifestError.message}`);
47
- // FIX: Passing { durationMs: 0 } instead of {} to satisfy type requirements
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 = failReason.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} ${date} (Attempt ${retryCount})`);
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
- logger.log('ERROR', `[Worker] ❌ Crash: ${computation} for ${date}: ${err.message}`);
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
- 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.273",
3
+ "version": "1.0.275",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [