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.
- package/functions/computation-system/helpers/computation_dispatcher.js +82 -44
- package/functions/computation-system/helpers/computation_worker.js +48 -43
- package/functions/computation-system/onboarding.md +712 -503
- package/functions/computation-system/persistence/ResultCommitter.js +160 -76
- package/functions/computation-system/persistence/RunRecorder.js +123 -28
- 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,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
|
|
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
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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)
|
|
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
|
}
|