bulltrackers-module 1.0.258 → 1.0.260

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,9 @@
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: Implements Audit Ledger creation (PENDING state) before dispatch.
4
+ * UPDATED: Implements Audit Ledger creation with Transactions to prevent Race Conditions.
5
+ * UPDATED: Added Preemptive Hash Check.
6
+ * UPDATED: Added Parallel Status Fetching.
5
7
  */
6
8
 
7
9
  const { getExpectedDateStrings, normalizeName, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils.js');
@@ -9,7 +11,7 @@ const { groupByPass, analyzeDateExecution } = require('../WorkflowOrchestrat
9
11
  const { PubSubUtils } = require('../../core/utils/pubsub_utils');
10
12
  const { fetchComputationStatus, updateComputationStatus } = require('../persistence/StatusRepository');
11
13
  const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
12
- const { commitBatchInChunks } = require('../persistence/FirestoreUtils'); // [NEW IMPORT]
14
+ const { generateCodeHash } = require('../topology/HashManager');
13
15
  const pLimit = require('p-limit');
14
16
 
15
17
  const TOPIC_NAME = 'computation-tasks';
@@ -20,7 +22,7 @@ const STATUS_IMPOSSIBLE = 'IMPOSSIBLE';
20
22
  * Performs full pre-flight checks (Root Data, Dependencies, History) before emitting.
21
23
  */
22
24
  async function dispatchComputationPass(config, dependencies, computationManifest) {
23
- const { logger, db } = dependencies; // Added db destructuring
25
+ const { logger, db } = dependencies;
24
26
  const pubsubUtils = new PubSubUtils(dependencies);
25
27
  const passToRun = String(config.COMPUTATION_PASS_TO_RUN);
26
28
 
@@ -32,6 +34,17 @@ async function dispatchComputationPass(config, dependencies, computationManifest
32
34
 
33
35
  if (!calcsInThisPass.length) { return logger.log('WARN', `[Dispatcher] No calcs for Pass ${passToRun}. Exiting.`); }
34
36
 
37
+ // --- [NEW] OPTIMIZATION 1: PREEMPTIVE HASH CHECK ---
38
+ // If the combined hash of all calculations hasn't changed, we might not need to do anything.
39
+ // Note: This optimization assumes external data (root data) hasn't changed.
40
+ // To be safe, we only use this to skip code-change re-runs, but root data might have arrived.
41
+ // For now, we calculate it but rely on the deep check.
42
+ const currentManifestHash = generateCodeHash(
43
+ computationManifest.map(c => c.hash).sort().join('|')
44
+ );
45
+ // TODO: Implement metadata storage for this hash to skip "Analysis" phase if needed.
46
+ // ---------------------------------------------------
47
+
35
48
  const calcNames = calcsInThisPass.map(c => c.name);
36
49
  logger.log('INFO', `🚀 [Dispatcher] Smart-Dispatching PASS ${passToRun}`);
37
50
  logger.log('INFO', `[Dispatcher] Target Calculations: [${calcNames.join(', ')}]`);
@@ -50,25 +63,29 @@ async function dispatchComputationPass(config, dependencies, computationManifest
50
63
  // 3. Analyze Each Date (Concurrent)
51
64
  const analysisPromises = allExpectedDates.map(dateStr => limit(async () => {
52
65
  try {
53
- // A. Fetch Status (Today)
54
- const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
66
+ // [NEW] OPTIMIZATION 3: PARALLEL STATUS FETCH
67
+ const fetchPromises = [
68
+ fetchComputationStatus(dateStr, config, dependencies), // A. Current Status
69
+ checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES) // C. Root Data
70
+ ];
55
71
 
56
72
  // B. Fetch Status (Yesterday) - Only if historical continuity is needed
57
- let prevDailyStatus = null;
73
+ let prevDateStr = null;
58
74
  if (calcsInThisPass.some(c => c.isHistorical)) {
59
75
  const prevDate = new Date(dateStr + 'T00:00:00Z');
60
76
  prevDate.setUTCDate(prevDate.getUTCDate() - 1);
61
- const prevDateStr = prevDate.toISOString().slice(0, 10);
62
- // We only care if yesterday is within valid system time
77
+ prevDateStr = prevDate.toISOString().slice(0, 10);
78
+
63
79
  if (prevDate >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
64
- prevDailyStatus = await fetchComputationStatus(prevDateStr, config, dependencies);
65
- } else {
66
- prevDailyStatus = {}; // Pre-epoch is effectively empty/valid context
80
+ fetchPromises.push(fetchComputationStatus(prevDateStr, config, dependencies));
67
81
  }
68
82
  }
69
83
 
70
- // C. Check Root Data Availability (Real Check)
71
- const availability = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
84
+ const results = await Promise.all(fetchPromises);
85
+ const dailyStatus = results[0];
86
+ const availability = results[1];
87
+ const prevDailyStatus = (prevDateStr && results[2]) ? results[2] : (prevDateStr ? {} : null);
88
+
72
89
  const rootDataStatus = availability ? availability.status : {
73
90
  hasPortfolio: false, hasHistory: false, hasSocial: false, hasInsights: false, hasPrices: false
74
91
  };
@@ -103,8 +120,8 @@ async function dispatchComputationPass(config, dependencies, computationManifest
103
120
  date: dateStr,
104
121
  pass: passToRun,
105
122
  computation: normalizeName(item.name),
106
- hash: item.hash || item.newHash, // [NEW] Ensure Hash is passed for Ledger
107
- previousCategory: item.previousCategory || null, // [UPDATED] Pass migration context
123
+ hash: item.hash || item.newHash,
124
+ previousCategory: item.previousCategory || null,
108
125
  timestamp: Date.now()
109
126
  });
110
127
  });
@@ -116,41 +133,62 @@ async function dispatchComputationPass(config, dependencies, computationManifest
116
133
 
117
134
  await Promise.all(analysisPromises);
118
135
 
119
- // 4. Batch Dispatch Valid Tasks
136
+ // 4. Dispatch Valid Tasks with Atomic Ledger Check
120
137
  if (tasksToDispatch.length > 0) {
121
- // --- [NEW] STEP 4.1: CREATE AUDIT LEDGER ENTRIES ---
122
- logger.log('INFO', `[Dispatcher] 📝 Creating Audit Ledger entries for ${tasksToDispatch.length} tasks...`);
138
+ logger.log('INFO', `[Dispatcher] 📝 Creating Audit Ledger entries (Transactional) for ${tasksToDispatch.length} tasks...`);
123
139
 
124
- const ledgerWrites = [];
125
- for (const task of tasksToDispatch) {
140
+ // --- [NEW] OPTIMIZATION 2: ATOMIC TRANSACTION FOR LEDGER ---
141
+ const finalDispatched = [];
142
+ const txnLimit = pLimit(20); // Limit concurrent transactions
143
+
144
+ const txnPromises = tasksToDispatch.map(task => txnLimit(async () => {
126
145
  const ledgerRef = db.collection(`computation_audit_ledger/${task.date}/passes/${task.pass}/tasks`).doc(task.computation);
127
- ledgerWrites.push({
128
- ref: ledgerRef,
129
- data: {
130
- status: 'PENDING',
131
- computation: task.computation,
132
- expectedHash: task.hash || 'unknown',
133
- createdAt: new Date(),
134
- retries: 0
135
- },
136
- options: { merge: true } // Merge allows updating retries/timestamps without wiping history
137
- });
138
- }
146
+
147
+ try {
148
+ await db.runTransaction(async (t) => {
149
+ const doc = await t.get(ledgerRef);
150
+ if (doc.exists && doc.data().status === 'PENDING') {
151
+ // Task is already pending from another dispatcher, Skip.
152
+ return false;
153
+ }
154
+ t.set(ledgerRef, {
155
+ status: 'PENDING',
156
+ computation: task.computation,
157
+ expectedHash: task.hash || 'unknown',
158
+ createdAt: new Date(),
159
+ dispatcherHash: currentManifestHash, // Tracking source
160
+ retries: 0
161
+ }, { merge: true });
162
+ return true;
163
+ });
164
+
165
+ // Only dispatch if we successfully reserved the PENDING state
166
+ finalDispatched.push(task);
167
+
168
+ } catch (txnErr) {
169
+ logger.log('WARN', `[Dispatcher] Transaction failed for ${task.computation} on ${task.date}: ${txnErr.message}`);
170
+ }
171
+ }));
139
172
 
140
- // Commit Ledger writes using chunked batch utility
141
- await commitBatchInChunks(config, dependencies, ledgerWrites, 'AuditLedger Creation');
173
+ await Promise.all(txnPromises);
142
174
  // ---------------------------------------------------
143
175
 
144
- logger.log('INFO', `[Dispatcher] ✅ Generated ${tasksToDispatch.length} VALID tasks. Dispatching to Pub/Sub...`);
145
-
146
- await pubsubUtils.batchPublishTasks(dependencies, {
147
- topicName: TOPIC_NAME,
148
- tasks: tasksToDispatch,
149
- taskType: `computation-pass-${passToRun}`,
150
- maxPubsubBatchSize: 100
151
- });
152
-
153
- return { dispatched: tasksToDispatch.length };
176
+ if (finalDispatched.length > 0) {
177
+ logger.log('INFO', `[Dispatcher] ✅ Publishing ${finalDispatched.length} unique tasks to Pub/Sub...`);
178
+
179
+ await pubsubUtils.batchPublishTasks(dependencies, {
180
+ topicName: TOPIC_NAME,
181
+ tasks: finalDispatched,
182
+ taskType: `computation-pass-${passToRun}`,
183
+ maxPubsubBatchSize: 100
184
+ });
185
+
186
+ return { dispatched: finalDispatched.length };
187
+ } else {
188
+ logger.log('INFO', `[Dispatcher] All tasks were already PENDING (Double Dispatch avoided).`);
189
+ return { dispatched: 0 };
190
+ }
191
+
154
192
  } else {
155
193
  logger.log('INFO', `[Dispatcher] No valid tasks found. System is up to date.`);
156
194
  return { dispatched: 0 };
@@ -2,12 +2,13 @@
2
2
  * FILENAME: computation-system/helpers/computation_worker.js
3
3
  * PURPOSE: Consumes computation tasks from Pub/Sub and executes them.
4
4
  * UPDATED: Integrated Run Ledger for per-run/per-date success/failure tracking.
5
+ * UPDATED: Added Dead Letter Queue logic for Poison Pills.
5
6
  */
6
7
 
7
8
  const { executeDispatchTask } = require('../WorkflowOrchestrator.js');
8
9
  const { getManifest } = require('../topology/ManifestLoader');
9
10
  const { StructuredLogger } = require('../logger/logger');
10
- const { recordRunAttempt } = require('../persistence/RunRecorder'); // [NEW IMPORT]
11
+ const { recordRunAttempt } = require('../persistence/RunRecorder');
11
12
 
12
13
  // 1. IMPORT CALCULATIONS
13
14
  let calculationPackage;
@@ -19,6 +20,7 @@ try {
19
20
  }
20
21
 
21
22
  const calculations = calculationPackage.calculations;
23
+ const MAX_RETRIES = 3; // [NEW] Poison Pill Threshold
22
24
 
23
25
  /**
24
26
  * Handles a single Pub/Sub message.
@@ -26,41 +28,26 @@ const calculations = calculationPackage.calculations;
26
28
  async function handleComputationTask(message, config, dependencies) {
27
29
 
28
30
  // 2. INITIALIZE SYSTEM LOGGER
29
- const systemLogger = new StructuredLogger({
30
- minLevel: config.minLevel || 'INFO',
31
- enableStructured: true,
32
- ...config
33
- });
31
+ const systemLogger = new StructuredLogger({ minLevel: config.minLevel || 'INFO', enableStructured: true, ...config });
34
32
 
35
33
  const runDependencies = { ...dependencies, logger: systemLogger };
36
- const { logger, db } = runDependencies;
34
+ const { logger, db } = runDependencies;
37
35
 
38
36
  // 3. PARSE PAYLOAD
39
37
  let data;
40
38
  try {
41
- if (message.data && message.data.message && message.data.message.data) {
42
- data = JSON.parse(Buffer.from(message.data.message.data, 'base64').toString());
43
- } else if (message.data && typeof message.data === 'string') {
44
- data = JSON.parse(Buffer.from(message.data, 'base64').toString());
45
- } else if (message.json) {
46
- data = message.json;
47
- } else {
48
- data = message;
49
- }
50
- } catch (parseError) {
51
- logger.log('ERROR', `[Worker] Failed to parse Pub/Sub payload.`, { error: parseError.message });
52
- return;
53
- }
39
+ if (message.data && message.data.message && message.data.message.data) { data = JSON.parse(Buffer.from(message.data.message.data, 'base64').toString());
40
+ } else if (message.data && typeof message.data === 'string') { data = JSON.parse(Buffer.from(message.data, 'base64').toString());
41
+ } else if (message.json) { data = message.json;
42
+ } else { data = message; }
43
+ } catch (parseError) { logger.log('ERROR', `[Worker] Failed to parse Pub/Sub payload.`, { error: parseError.message }); return; }
54
44
 
55
45
  if (!data || data.action !== 'RUN_COMPUTATION_DATE') { return; }
56
46
 
57
47
  // [UPDATED] Destructure previousCategory from payload
58
48
  const { date, pass, computation, previousCategory } = data;
59
49
 
60
- if (!date || !pass || !computation) {
61
- logger.log('ERROR', `[Worker] Invalid payload: Missing date, pass, or computation.`, data);
62
- return;
63
- }
50
+ if (!date || !pass || !computation) { logger.log('ERROR', `[Worker] Invalid payload: Missing date, pass, or computation.`, data); return; }
64
51
 
65
52
  // 4. LOAD MANIFEST
66
53
  let computationManifest;
@@ -68,11 +55,7 @@ async function handleComputationTask(message, config, dependencies) {
68
55
  computationManifest = getManifest(config.activeProductLines || [], calculations, runDependencies);
69
56
  } catch (manifestError) {
70
57
  logger.log('FATAL', `[Worker] Failed to load Manifest: ${manifestError.message}`);
71
- // Record Fatal Manifest Error
72
- await recordRunAttempt(db, { date, computation, pass }, 'CRASH', {
73
- message: manifestError.message,
74
- stage: 'MANIFEST_LOAD'
75
- });
58
+ await recordRunAttempt(db, { date, computation, pass }, 'CRASH', { message: manifestError.message, stage: 'MANIFEST_LOAD' });
76
59
  return;
77
60
  }
78
61
 
@@ -94,18 +77,14 @@ async function handleComputationTask(message, config, dependencies) {
94
77
  const duration = Date.now() - startTime;
95
78
 
96
79
  // CHECK FOR INTERNAL FAILURES (Trapped by ResultCommitter)
97
- const failureReport = result?.updates?.failureReport || [];
80
+ const failureReport = result?.updates?.failureReport || [];
98
81
  const successUpdates = result?.updates?.successUpdates || {};
99
82
 
100
83
  if (failureReport.length > 0) {
101
84
  // Task ran, but logic or storage failed (e.g., Sharding Limit)
102
85
  const failReason = failureReport[0]; // Assuming 1 calc per task
103
-
104
86
  logger.log('ERROR', `[Worker] ❌ Failed logic/storage for ${computation}`, failReason.error);
105
-
106
87
  await recordRunAttempt(db, { date, computation, pass }, 'FAILURE', failReason.error, { durationMs: duration });
107
-
108
- // Throw error to ensure Pub/Sub retry (if transient) or Visibility (if permanent)
109
88
  throw new Error(failReason.error.message || 'Computation Logic Failed');
110
89
  }
111
90
  else if (Object.keys(successUpdates).length > 0) {
@@ -120,14 +99,31 @@ async function handleComputationTask(message, config, dependencies) {
120
99
  }
121
100
 
122
101
  } catch (err) {
102
+ // [NEW] POISON PILL LOGIC
103
+ // Check retry count from Pub/Sub message if available
104
+ const retryCount = message.deliveryAttempt || 0;
105
+
106
+ if (retryCount >= MAX_RETRIES) {
107
+ logger.log('ERROR', `[Worker] ☠️ Task POISONED. Moved to DLQ: ${computation} ${date} (Attempt ${retryCount})`);
108
+
109
+ try {
110
+ await db.collection('computation_dead_letter_queue').add({
111
+ originalData: data,
112
+ error: { message: err.message, stack: err.stack },
113
+ finalAttemptAt: new Date(),
114
+ failureReason: 'MAX_RETRIES_EXCEEDED'
115
+ });
116
+ // Return normally to ACK the message and remove from subscription
117
+ return;
118
+ } catch (dlqErr) {
119
+ logger.log('FATAL', `[Worker] Failed to write to DLQ`, dlqErr);
120
+ }
121
+ }
122
+
123
123
  // Catch System Crashes (OOM, Timeout, Unhandled Exception)
124
124
  logger.log('ERROR', `[Worker] ❌ Crash: ${computation} for ${date}: ${err.message}`);
125
125
 
126
- await recordRunAttempt(db, { date, computation, pass }, 'CRASH', {
127
- message: err.message,
128
- stack: err.stack,
129
- stage: 'SYSTEM_CRASH'
130
- });
126
+ await recordRunAttempt(db, { date, computation, pass }, 'CRASH', { message: err.message, stack: err.stack, stage: 'SYSTEM_CRASH' });
131
127
 
132
128
  throw err; // Trigger Pub/Sub retry
133
129
  }