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.
- package/functions/computation-system/helpers/computation_dispatcher.js +82 -44
- package/functions/computation-system/helpers/computation_worker.js +35 -39
- package/functions/computation-system/onboarding.md +712 -503
- package/functions/computation-system/persistence/ResultCommitter.js +127 -74
- package/functions/computation-system/tools/BuildReporter.js +28 -79
- package/functions/computation-system/utils/schema_capture.js +31 -2
- package/index.js +2 -4
- package/package.json +1 -1
|
@@ -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
|
|
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 {
|
|
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;
|
|
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
|
-
//
|
|
54
|
-
const
|
|
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
|
|
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
|
-
|
|
62
|
-
|
|
77
|
+
prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
78
|
+
|
|
63
79
|
if (prevDate >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
|
|
64
|
-
|
|
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
|
-
|
|
71
|
-
const
|
|
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,
|
|
107
|
-
previousCategory: item.previousCategory || null,
|
|
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.
|
|
136
|
+
// 4. Dispatch Valid Tasks with Atomic Ledger Check
|
|
120
137
|
if (tasksToDispatch.length > 0) {
|
|
121
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
await commitBatchInChunks(config, dependencies, ledgerWrites, 'AuditLedger Creation');
|
|
173
|
+
await Promise.all(txnPromises);
|
|
142
174
|
// ---------------------------------------------------
|
|
143
175
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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');
|
|
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 }
|
|
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
|
-
|
|
43
|
-
} else if (message.data
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|