bulltrackers-module 1.0.259 → 1.0.261

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,40 +77,62 @@ 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
- // Task ran, but logic or storage failed (e.g., Sharding Limit)
84
+ // Task ran, but logic or storage failed
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
87
 
106
- 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)
88
+ // Extract any metrics gathered before failure (e.g., anomalies)
89
+ const metrics = failReason.metrics || {};
90
+ metrics.durationMs = duration;
91
+
92
+ await recordRunAttempt(db, { date, computation, pass }, 'FAILURE', failReason.error, metrics);
109
93
  throw new Error(failReason.error.message || 'Computation Logic Failed');
110
94
  }
111
95
  else if (Object.keys(successUpdates).length > 0) {
112
96
  // Success
113
- logger.log('INFO', `[Worker] Stored: ${computation} for ${date}`);
114
- await recordRunAttempt(db, { date, computation, pass }, 'SUCCESS', null, { durationMs: duration });
97
+ const successData = successUpdates[computation]; // Extract specific calc data
98
+ const metrics = successData.metrics || {};
99
+ metrics.durationMs = duration;
100
+
101
+ logger.log('INFO', `[Worker] ✅ Stored: ${computation} for ${date} (${metrics.storage?.sizeBytes} bytes)`);
102
+ await recordRunAttempt(db, { date, computation, pass }, 'SUCCESS', null, metrics);
115
103
  }
116
104
  else {
117
- // No updates, but no error (e.g. Empty Result) - Log as Success/Skipped
105
+ // No updates, but no error (e.g. Empty Result)
118
106
  logger.log('WARN', `[Worker] ⚠️ No results produced for ${computation} (Empty?)`);
119
107
  await recordRunAttempt(db, { date, computation, pass }, 'SUCCESS', { message: 'Empty Result' }, { durationMs: duration });
120
108
  }
121
109
 
122
110
  } catch (err) {
111
+ // [NEW] POISON PILL LOGIC
112
+ // Check retry count from Pub/Sub message if available
113
+ const retryCount = message.deliveryAttempt || 0;
114
+
115
+ if (retryCount >= MAX_RETRIES) {
116
+ logger.log('ERROR', `[Worker] ☠️ Task POISONED. Moved to DLQ: ${computation} ${date} (Attempt ${retryCount})`);
117
+
118
+ try {
119
+ await db.collection('computation_dead_letter_queue').add({
120
+ originalData: data,
121
+ error: { message: err.message, stack: err.stack },
122
+ finalAttemptAt: new Date(),
123
+ failureReason: 'MAX_RETRIES_EXCEEDED'
124
+ });
125
+ // Return normally to ACK the message and remove from subscription
126
+ return;
127
+ } catch (dlqErr) {
128
+ logger.log('FATAL', `[Worker] Failed to write to DLQ`, dlqErr);
129
+ }
130
+ }
131
+
123
132
  // Catch System Crashes (OOM, Timeout, Unhandled Exception)
124
133
  logger.log('ERROR', `[Worker] ❌ Crash: ${computation} for ${date}: ${err.message}`);
125
134
 
126
- await recordRunAttempt(db, { date, computation, pass }, 'CRASH', {
127
- message: err.message,
128
- stack: err.stack,
129
- stage: 'SYSTEM_CRASH'
130
- });
135
+ await recordRunAttempt(db, { date, computation, pass }, 'CRASH', { message: err.message, stack: err.stack, stage: 'SYSTEM_CRASH' });
131
136
 
132
137
  throw err; // Trigger Pub/Sub retry
133
138
  }