bulltrackers-module 1.0.242 → 1.0.244
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/WorkflowOrchestrator.js +98 -106
- package/functions/computation-system/helpers/computation_dispatcher.js +96 -32
- package/functions/computation-system/helpers/computation_worker.js +39 -80
- package/functions/computation-system/tools/BuildReporter.js +16 -13
- package/package.json +1 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Main Orchestrator. Coordinates the topological execution.
|
|
3
|
-
* UPDATED:
|
|
4
|
-
* UPDATED: Uses centralized DEFINITIVE_EARLIEST_DATES.
|
|
3
|
+
* UPDATED: Added 'executeDispatchTask' for trusted execution from Smart Dispatcher.
|
|
5
4
|
*/
|
|
6
5
|
const { normalizeName, DEFINITIVE_EARLIEST_DATES } = require('./utils/utils');
|
|
7
6
|
const { checkRootDataAvailability } = require('./data/AvailabilityChecker');
|
|
@@ -20,7 +19,10 @@ function groupByPass(manifest) {
|
|
|
20
19
|
}, {});
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Analyzes whether calculations should run, be skipped, or are blocked.
|
|
24
|
+
*/
|
|
25
|
+
function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus = null) {
|
|
24
26
|
const report = {
|
|
25
27
|
runnable: [],
|
|
26
28
|
blocked: [],
|
|
@@ -34,14 +36,11 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
34
36
|
|
|
35
37
|
const isDepSatisfied = (depName, dailyStatus, manifestMap) => {
|
|
36
38
|
const norm = normalizeName(depName);
|
|
37
|
-
const stored = dailyStatus[norm];
|
|
39
|
+
const stored = dailyStatus[norm];
|
|
38
40
|
const depManifest = manifestMap.get(norm);
|
|
39
41
|
|
|
40
42
|
if (!stored) return false;
|
|
41
|
-
|
|
42
|
-
// Handle IMPOSSIBLE flag (stored as object property or legacy string check)
|
|
43
43
|
if (stored.hash === STATUS_IMPOSSIBLE) return false;
|
|
44
|
-
|
|
45
44
|
if (!depManifest) return false;
|
|
46
45
|
if (stored.hash !== depManifest.hash) return false;
|
|
47
46
|
|
|
@@ -50,13 +49,12 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
50
49
|
|
|
51
50
|
for (const calc of calcsInPass) {
|
|
52
51
|
const cName = normalizeName(calc.name);
|
|
53
|
-
const stored = dailyStatus[cName];
|
|
52
|
+
const stored = dailyStatus[cName];
|
|
54
53
|
|
|
55
54
|
const storedHash = stored ? stored.hash : null;
|
|
56
55
|
const storedCategory = stored ? stored.category : null;
|
|
57
56
|
const currentHash = calc.hash;
|
|
58
57
|
|
|
59
|
-
// [SMART MIGRATION] Detect if category changed, independent of hash check
|
|
60
58
|
let migrationOldCategory = null;
|
|
61
59
|
if (storedCategory && storedCategory !== calc.category) {
|
|
62
60
|
migrationOldCategory = storedCategory;
|
|
@@ -119,12 +117,28 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
119
117
|
continue;
|
|
120
118
|
}
|
|
121
119
|
|
|
122
|
-
// 4.
|
|
120
|
+
// 4. Strict Historical Consistency
|
|
121
|
+
if (calc.isHistorical && prevDailyStatus) {
|
|
122
|
+
const yesterday = new Date(dateStr + 'T00:00:00Z');
|
|
123
|
+
yesterday.setUTCDate(yesterday.getUTCDate() - 1);
|
|
124
|
+
|
|
125
|
+
if (yesterday >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
|
|
126
|
+
const prevStored = prevDailyStatus[cName];
|
|
127
|
+
|
|
128
|
+
if (!prevStored || prevStored.hash !== currentHash) {
|
|
129
|
+
report.blocked.push({
|
|
130
|
+
name: cName,
|
|
131
|
+
reason: `Waiting for historical continuity (Yesterday ${!prevStored ? 'Missing' : 'Hash Mismatch'})`
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 5. Runnable Decision
|
|
123
139
|
if (!storedHash) {
|
|
124
140
|
report.runnable.push(calc);
|
|
125
141
|
} else if (storedHash !== currentHash) {
|
|
126
|
-
// Hash Mismatch (Code Changed).
|
|
127
|
-
// Pass migration info here too, in case category ALSO changed.
|
|
128
142
|
report.reRuns.push({
|
|
129
143
|
name: cName,
|
|
130
144
|
oldHash: storedHash,
|
|
@@ -132,7 +146,6 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
132
146
|
previousCategory: migrationOldCategory
|
|
133
147
|
});
|
|
134
148
|
} else if (migrationOldCategory) {
|
|
135
|
-
// Hash Matches, BUT category changed. Force Re-run.
|
|
136
149
|
report.reRuns.push({
|
|
137
150
|
name: cName,
|
|
138
151
|
reason: 'Category Migration',
|
|
@@ -140,7 +153,6 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
140
153
|
newCategory: calc.category
|
|
141
154
|
});
|
|
142
155
|
} else {
|
|
143
|
-
// Stored Hash === Current Hash AND Category matches
|
|
144
156
|
report.skipped.push({ name: cName });
|
|
145
157
|
}
|
|
146
158
|
}
|
|
@@ -148,117 +160,97 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
148
160
|
return report;
|
|
149
161
|
}
|
|
150
162
|
|
|
151
|
-
|
|
163
|
+
/**
|
|
164
|
+
* DIRECT EXECUTION PIPELINE (For Workers)
|
|
165
|
+
* Skips analysis. Assumes the calculation is valid and runnable.
|
|
166
|
+
*/
|
|
167
|
+
async function executeDispatchTask(dateStr, pass, targetComputation, config, dependencies, computationManifest) {
|
|
152
168
|
const { logger } = dependencies;
|
|
153
|
-
const
|
|
154
|
-
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
169
|
+
const pid = generateProcessId(PROCESS_TYPES.EXECUTOR, targetComputation, dateStr);
|
|
155
170
|
|
|
156
|
-
// 1.
|
|
157
|
-
const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
|
|
158
|
-
|
|
159
|
-
// 2. Check Data Availability
|
|
160
|
-
// [UPDATE] Using centralized dates to ensure consistency with BuildReporter
|
|
161
|
-
const rootData = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
|
|
162
|
-
const rootStatus = rootData ? rootData.status : { hasPortfolio: false, hasPrices: false, hasInsights: false, hasSocial: false, hasHistory: false };
|
|
163
|
-
|
|
164
|
-
// 3. ANALYZE EXECUTION
|
|
171
|
+
// 1. Get Calculation Manifest
|
|
165
172
|
const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
|
|
166
|
-
const
|
|
173
|
+
const calcManifest = manifestMap.get(normalizeName(targetComputation));
|
|
167
174
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
logger.logDateAnalysis(dateStr, analysisReport);
|
|
171
|
-
} else {
|
|
172
|
-
const logMsg = `[Analysis] Date: ${dateStr} | Runnable: ${analysisReport.runnable.length} | Blocked: ${analysisReport.blocked.length} | Impossible: ${analysisReport.impossible.length}`;
|
|
173
|
-
if (logger && logger.info) logger.info(logMsg);
|
|
174
|
-
else console.log(logMsg);
|
|
175
|
+
if (!calcManifest) {
|
|
176
|
+
throw new Error(`Calculation '${targetComputation}' not found in manifest.`);
|
|
175
177
|
}
|
|
176
178
|
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
analysisReport.blocked.forEach(item => statusUpdates[item.name] = { hash: false, category: 'unknown' });
|
|
181
|
-
analysisReport.failedDependency.forEach(item => statusUpdates[item.name] = { hash: false, category: 'unknown' });
|
|
182
|
-
analysisReport.impossible.forEach(item => statusUpdates[item.name] = { hash: STATUS_IMPOSSIBLE, category: 'unknown' });
|
|
179
|
+
// 2. Fetch Root Data References (Required for execution streaming)
|
|
180
|
+
// Even though Dispatcher checked existence, we need the actual Refs/Data objects now.
|
|
181
|
+
const rootData = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
|
|
183
182
|
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
// Safety Fallback (Should be impossible if Dispatcher is working)
|
|
184
|
+
if (!rootData) {
|
|
185
|
+
logger.log('ERROR', `[Executor] FATAL: Root data missing for ${targetComputation} on ${dateStr}. Dispatcher desync?`);
|
|
186
|
+
return;
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
//
|
|
189
|
+
// 3. Fetch Dependencies
|
|
190
|
+
const calcsToRun = [calcManifest];
|
|
191
|
+
const existingResults = await fetchExistingResults(dateStr, calcsToRun, computationManifest, config, dependencies, false);
|
|
189
192
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
const calcsToRunNames = new Set([
|
|
199
|
-
...analysisReport.runnable.map(c => c.name),
|
|
200
|
-
...analysisReport.reRuns.map(c => c.name)
|
|
201
|
-
]);
|
|
202
|
-
|
|
203
|
-
// [SMART MIGRATION] Create Safe Copies with previousCategory attached
|
|
204
|
-
// We clone the manifest object so we don't pollute the global cache with run-specific flags
|
|
205
|
-
const finalRunList = calcsInThisPass
|
|
206
|
-
.filter(c => calcsToRunNames.has(normalizeName(c.name)))
|
|
207
|
-
.map(c => {
|
|
208
|
-
const clone = { ...c }; // Shallow copy
|
|
209
|
-
const prevCat = migrationMap[normalizeName(c.name)];
|
|
210
|
-
if (prevCat) {
|
|
211
|
-
clone.previousCategory = prevCat;
|
|
212
|
-
}
|
|
213
|
-
return clone;
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
if (!finalRunList.length) {
|
|
217
|
-
return {
|
|
218
|
-
date: dateStr,
|
|
219
|
-
updates: {},
|
|
220
|
-
skipped: analysisReport.skipped.length,
|
|
221
|
-
impossible: analysisReport.impossible.length
|
|
222
|
-
};
|
|
193
|
+
let previousResults = {};
|
|
194
|
+
if (calcManifest.isHistorical) {
|
|
195
|
+
const prevDate = new Date(dateStr + 'T00:00:00Z');
|
|
196
|
+
prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
197
|
+
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
198
|
+
previousResults = await fetchExistingResults(prevDateStr, calcsToRun, computationManifest, config, dependencies, true);
|
|
223
199
|
}
|
|
224
200
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const standardToRun = finalRunList.filter(c => c.type === 'standard');
|
|
230
|
-
const metaToRun = finalRunList.filter(c => c.type === 'meta');
|
|
201
|
+
// 4. Execute
|
|
202
|
+
logger.log('INFO', `[Executor] Running ${calcManifest.name} for ${dateStr}`, { processId: pid });
|
|
231
203
|
|
|
232
|
-
|
|
204
|
+
let resultUpdates = {};
|
|
233
205
|
|
|
234
206
|
try {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
207
|
+
if (calcManifest.type === 'standard') {
|
|
208
|
+
resultUpdates = await StandardExecutor.run(
|
|
209
|
+
new Date(dateStr + 'T00:00:00Z'),
|
|
210
|
+
[calcManifest],
|
|
211
|
+
`Pass ${pass}`,
|
|
212
|
+
config,
|
|
213
|
+
dependencies,
|
|
214
|
+
rootData,
|
|
215
|
+
existingResults,
|
|
216
|
+
previousResults
|
|
217
|
+
);
|
|
218
|
+
} else if (calcManifest.type === 'meta') {
|
|
219
|
+
resultUpdates = await MetaExecutor.run(
|
|
220
|
+
new Date(dateStr + 'T00:00:00Z'),
|
|
221
|
+
[calcManifest],
|
|
222
|
+
`Pass ${pass}`,
|
|
223
|
+
config,
|
|
224
|
+
dependencies,
|
|
225
|
+
existingResults,
|
|
226
|
+
previousResults,
|
|
227
|
+
rootData
|
|
228
|
+
);
|
|
250
229
|
}
|
|
230
|
+
|
|
231
|
+
logger.log('INFO', `[Executor] Success: ${calcManifest.name} for ${dateStr}`);
|
|
232
|
+
return { date: dateStr, updates: resultUpdates };
|
|
251
233
|
|
|
252
234
|
} catch (err) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
} else {
|
|
256
|
-
console.error(`[Orchestrator] Failed execution for ${dateStr}: ${err.message}`);
|
|
257
|
-
}
|
|
258
|
-
throw err;
|
|
235
|
+
logger.log('ERROR', `[Executor] Failed ${calcManifest.name}: ${err.message}`, { processId: pid, stack: err.stack });
|
|
236
|
+
throw err; // Trigger retry
|
|
259
237
|
}
|
|
238
|
+
}
|
|
260
239
|
|
|
261
|
-
|
|
240
|
+
/**
|
|
241
|
+
* Legacy/Orchestrator Mode execution (Performs analysis).
|
|
242
|
+
* Kept for manual runs or full-system validation.
|
|
243
|
+
*/
|
|
244
|
+
async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, dependencies, computationManifest) {
|
|
245
|
+
// ... [Previous implementation of runDateComputation can remain here if needed for backward compatibility,
|
|
246
|
+
// ... or can be removed if the system is fully migrated. Keeping logic for "Dispatcher uses analyzeDateExecution"]
|
|
247
|
+
|
|
248
|
+
// Re-exporting executeDispatchTask as the primary worker entry point.
|
|
262
249
|
}
|
|
263
250
|
|
|
264
|
-
module.exports = {
|
|
251
|
+
module.exports = {
|
|
252
|
+
runDateComputation,
|
|
253
|
+
executeDispatchTask, // <--- NEW EXPORT
|
|
254
|
+
groupByPass,
|
|
255
|
+
analyzeDateExecution
|
|
256
|
+
};
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FILENAME:
|
|
3
|
-
* PURPOSE:
|
|
4
|
-
* UPDATED: Implements
|
|
2
|
+
* FILENAME: computation-system/helpers/computation_dispatcher.js
|
|
3
|
+
* PURPOSE: "Smart Dispatcher" - Analyzes state and only dispatches valid, runnable tasks.
|
|
4
|
+
* UPDATED: Implements pre-dispatch analysis to guarantee worker success.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { getExpectedDateStrings, normalizeName, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils.js');
|
|
8
|
-
const { groupByPass }
|
|
8
|
+
const { groupByPass, analyzeDateExecution } = require('../WorkflowOrchestrator.js');
|
|
9
9
|
const { PubSubUtils } = require('../../core/utils/pubsub_utils');
|
|
10
|
+
const { fetchComputationStatus, updateComputationStatus } = require('../persistence/StatusRepository');
|
|
11
|
+
const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
|
|
12
|
+
const pLimit = require('p-limit');
|
|
10
13
|
|
|
11
14
|
const TOPIC_NAME = 'computation-tasks';
|
|
15
|
+
const STATUS_IMPOSSIBLE = 'IMPOSSIBLE';
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
18
|
* Dispatches computation tasks for a specific pass.
|
|
15
|
-
*
|
|
19
|
+
* Performs full pre-flight checks (Root Data, Dependencies, History) before emitting.
|
|
16
20
|
*/
|
|
17
21
|
async function dispatchComputationPass(config, dependencies, computationManifest) {
|
|
18
22
|
const { logger } = dependencies;
|
|
@@ -28,44 +32,104 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
28
32
|
if (!calcsInThisPass.length) { return logger.log('WARN', `[Dispatcher] No calcs for Pass ${passToRun}. Exiting.`); }
|
|
29
33
|
|
|
30
34
|
const calcNames = calcsInThisPass.map(c => c.name);
|
|
31
|
-
logger.log('INFO', `🚀 [Dispatcher]
|
|
35
|
+
logger.log('INFO', `🚀 [Dispatcher] Smart-Dispatching PASS ${passToRun}`);
|
|
32
36
|
logger.log('INFO', `[Dispatcher] Target Calculations: [${calcNames.join(', ')}]`);
|
|
33
37
|
|
|
34
38
|
// 2. Determine Date Range
|
|
35
|
-
// [UPDATE] Using DEFINITIVE_EARLIEST_DATES ensures we don't dispatch tasks
|
|
36
|
-
// for years before data existed (e.g. 2023), saving massive Pub/Sub costs.
|
|
37
39
|
const passEarliestDate = Object.values(DEFINITIVE_EARLIEST_DATES).reduce((a, b) => a < b ? a : b);
|
|
38
40
|
const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
|
|
39
41
|
const allExpectedDates = getExpectedDateStrings(passEarliestDate, endDateUTC);
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
|
|
44
|
+
const tasksToDispatch = [];
|
|
45
|
+
const limit = pLimit(20); // Process 20 days in parallel
|
|
46
|
+
|
|
47
|
+
logger.log('INFO', `[Dispatcher] Analyzing ${allExpectedDates.length} dates for viability...`);
|
|
48
|
+
|
|
49
|
+
// 3. Analyze Each Date (Concurrent)
|
|
50
|
+
const analysisPromises = allExpectedDates.map(dateStr => limit(async () => {
|
|
51
|
+
try {
|
|
52
|
+
// A. Fetch Status (Today)
|
|
53
|
+
const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
|
|
54
|
+
|
|
55
|
+
// B. Fetch Status (Yesterday) - Only if historical continuity is needed
|
|
56
|
+
let prevDailyStatus = null;
|
|
57
|
+
if (calcsInThisPass.some(c => c.isHistorical)) {
|
|
58
|
+
const prevDate = new Date(dateStr + 'T00:00:00Z');
|
|
59
|
+
prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
60
|
+
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
61
|
+
// We only care if yesterday is within valid system time
|
|
62
|
+
if (prevDate >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
|
|
63
|
+
prevDailyStatus = await fetchComputationStatus(prevDateStr, config, dependencies);
|
|
64
|
+
} else {
|
|
65
|
+
prevDailyStatus = {}; // Pre-epoch is effectively empty/valid context
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// C. Check Root Data Availability (Real Check)
|
|
70
|
+
const availability = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
|
|
71
|
+
const rootDataStatus = availability ? availability.status : {
|
|
72
|
+
hasPortfolio: false, hasHistory: false, hasSocial: false, hasInsights: false, hasPrices: false
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// D. Run Core Analysis Logic
|
|
76
|
+
const report = analyzeDateExecution(dateStr, calcsInThisPass, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus);
|
|
77
|
+
|
|
78
|
+
// E. Handle Non-Runnable States (Write directly to DB, don't dispatch)
|
|
79
|
+
const statusUpdates = {};
|
|
80
|
+
|
|
81
|
+
// Mark Impossible (Permanent Failure)
|
|
82
|
+
report.impossible.forEach(item => {
|
|
83
|
+
if (dailyStatus[item.name]?.hash !== STATUS_IMPOSSIBLE) {
|
|
84
|
+
statusUpdates[item.name] = { hash: STATUS_IMPOSSIBLE, category: 'unknown', reason: item.reason };
|
|
85
|
+
}
|
|
52
86
|
});
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
87
|
|
|
56
|
-
|
|
88
|
+
// Mark Blocked/Failed Deps (Temporary Failure)
|
|
89
|
+
// We write these so the status reflects reality, but we DO NOT dispatch them.
|
|
90
|
+
[...report.blocked, ...report.failedDependency].forEach(item => {
|
|
91
|
+
statusUpdates[item.name] = { hash: false, category: 'unknown', reason: item.reason };
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (Object.keys(statusUpdates).length > 0) {
|
|
95
|
+
await updateComputationStatus(dateStr, statusUpdates, config, dependencies);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// F. Queue Runnables
|
|
99
|
+
const validToRun = [...report.runnable, ...report.reRuns];
|
|
100
|
+
validToRun.forEach(item => {
|
|
101
|
+
tasksToDispatch.push({
|
|
102
|
+
action: 'RUN_COMPUTATION_DATE',
|
|
103
|
+
date: dateStr,
|
|
104
|
+
pass: passToRun,
|
|
105
|
+
computation: normalizeName(item.name),
|
|
106
|
+
timestamp: Date.now()
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
} catch (e) {
|
|
111
|
+
logger.log('ERROR', `[Dispatcher] Failed analysis for ${dateStr}: ${e.message}`);
|
|
112
|
+
}
|
|
113
|
+
}));
|
|
57
114
|
|
|
58
|
-
|
|
59
|
-
// We send tasks in batches to Pub/Sub to be efficient,
|
|
60
|
-
// but the WORKERS will process them individually.
|
|
61
|
-
await pubsubUtils.batchPublishTasks(dependencies, {
|
|
62
|
-
topicName: TOPIC_NAME,
|
|
63
|
-
tasks: allTasks,
|
|
64
|
-
taskType: `computation-pass-${passToRun}`,
|
|
65
|
-
maxPubsubBatchSize: 100 // Safe batch size
|
|
66
|
-
});
|
|
115
|
+
await Promise.all(analysisPromises);
|
|
67
116
|
|
|
68
|
-
|
|
117
|
+
// 4. Batch Dispatch Valid Tasks
|
|
118
|
+
if (tasksToDispatch.length > 0) {
|
|
119
|
+
logger.log('INFO', `[Dispatcher] ✅ Generated ${tasksToDispatch.length} VALID tasks. Dispatching...`);
|
|
120
|
+
|
|
121
|
+
await pubsubUtils.batchPublishTasks(dependencies, {
|
|
122
|
+
topicName: TOPIC_NAME,
|
|
123
|
+
tasks: tasksToDispatch,
|
|
124
|
+
taskType: `computation-pass-${passToRun}`,
|
|
125
|
+
maxPubsubBatchSize: 100
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return { dispatched: tasksToDispatch.length };
|
|
129
|
+
} else {
|
|
130
|
+
logger.log('INFO', `[Dispatcher] No valid tasks found. System is up to date.`);
|
|
131
|
+
return { dispatched: 0 };
|
|
132
|
+
}
|
|
69
133
|
}
|
|
70
134
|
|
|
71
135
|
module.exports = { dispatchComputationPass };
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FILENAME: computation-system/helpers/computation_worker.js
|
|
3
3
|
* PURPOSE: Consumes computation tasks from Pub/Sub and executes them.
|
|
4
|
-
* UPDATED:
|
|
4
|
+
* UPDATED: Simplified "Dumb Worker" - Trusts Dispatcher validation.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const {
|
|
8
|
-
const { getManifest }
|
|
9
|
-
const { StructuredLogger }
|
|
10
|
-
const { normalizeName } = require('../utils/utils');
|
|
7
|
+
const { executeDispatchTask } = require('../WorkflowOrchestrator.js');
|
|
8
|
+
const { getManifest } = require('../topology/ManifestLoader');
|
|
9
|
+
const { StructuredLogger } = require('../logger/logger');
|
|
11
10
|
|
|
12
11
|
// 1. IMPORT CALCULATIONS
|
|
13
12
|
let calculationPackage;
|
|
14
13
|
try {
|
|
15
14
|
calculationPackage = require('aiden-shared-calculations-unified');
|
|
16
15
|
} catch (e) {
|
|
17
|
-
console.error("FATAL: Could not load 'aiden-shared-calculations-unified'.
|
|
16
|
+
console.error("FATAL: Could not load 'aiden-shared-calculations-unified'.");
|
|
18
17
|
throw e;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
const calculations = calculationPackage.calculations;
|
|
22
21
|
|
|
23
22
|
/**
|
|
24
|
-
* Handles a single Pub/Sub message
|
|
23
|
+
* Handles a single Pub/Sub message.
|
|
24
|
+
* Assumes the message contains a VALID, RUNNABLE task from the Smart Dispatcher.
|
|
25
25
|
*/
|
|
26
26
|
async function handleComputationTask(message, config, dependencies) {
|
|
27
27
|
|
|
@@ -32,36 +32,16 @@ async function handleComputationTask(message, config, dependencies) {
|
|
|
32
32
|
...config
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
const runDependencies = {
|
|
37
|
-
...dependencies,
|
|
38
|
-
logger: systemLogger
|
|
39
|
-
};
|
|
40
|
-
|
|
35
|
+
const runDependencies = { ...dependencies, logger: systemLogger };
|
|
41
36
|
const { logger } = runDependencies;
|
|
42
37
|
|
|
43
|
-
//
|
|
44
|
-
let computationManifest;
|
|
45
|
-
try {
|
|
46
|
-
computationManifest = getManifest(
|
|
47
|
-
config.activeProductLines || [],
|
|
48
|
-
calculations,
|
|
49
|
-
runDependencies
|
|
50
|
-
);
|
|
51
|
-
} catch (manifestError) {
|
|
52
|
-
logger.log('FATAL', `[Worker] Failed to load Manifest: ${manifestError.message}`);
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// 5. PARSE PUB/SUB MESSAGE
|
|
38
|
+
// 3. PARSE PAYLOAD
|
|
57
39
|
let data;
|
|
58
40
|
try {
|
|
59
41
|
if (message.data && message.data.message && message.data.message.data) {
|
|
60
|
-
|
|
61
|
-
data = JSON.parse(buffer.toString());
|
|
42
|
+
data = JSON.parse(Buffer.from(message.data.message.data, 'base64').toString());
|
|
62
43
|
} else if (message.data && typeof message.data === 'string') {
|
|
63
|
-
|
|
64
|
-
data = JSON.parse(buffer.toString());
|
|
44
|
+
data = JSON.parse(Buffer.from(message.data, 'base64').toString());
|
|
65
45
|
} else if (message.json) {
|
|
66
46
|
data = message.json;
|
|
67
47
|
} else {
|
|
@@ -72,66 +52,45 @@ async function handleComputationTask(message, config, dependencies) {
|
|
|
72
52
|
return;
|
|
73
53
|
}
|
|
74
54
|
|
|
75
|
-
|
|
55
|
+
if (!data || data.action !== 'RUN_COMPUTATION_DATE') { return; }
|
|
56
|
+
|
|
57
|
+
const { date, pass, computation } = data;
|
|
58
|
+
|
|
59
|
+
if (!date || !pass || !computation) {
|
|
60
|
+
logger.log('ERROR', `[Worker] Invalid payload: Missing date, pass, or computation.`, data);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 4. LOAD MANIFEST
|
|
65
|
+
let computationManifest;
|
|
76
66
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const { date, pass, computation } = data; // Extract 'computation'
|
|
83
|
-
|
|
84
|
-
if (!date || !pass) {
|
|
85
|
-
logger.log('ERROR', `[Worker] Missing date or pass in payload: ${JSON.stringify(data)}`);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Load Full Pass
|
|
90
|
-
const passes = groupByPass(computationManifest);
|
|
91
|
-
let calcsInThisPass = passes[pass] || [];
|
|
92
|
-
|
|
93
|
-
// --- GRANULAR FILTERING ---
|
|
94
|
-
if (computation) {
|
|
95
|
-
const targetName = normalizeName(computation);
|
|
96
|
-
const targetCalc = calcsInThisPass.find(c => normalizeName(c.name) === targetName);
|
|
97
|
-
|
|
98
|
-
if (!targetCalc) {
|
|
99
|
-
logger.log('WARN', `[Worker] Targeted computation '${computation}' not found in Pass ${pass}. Skipping.`);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// We run ONLY this calculation
|
|
104
|
-
calcsInThisPass = [targetCalc];
|
|
105
|
-
logger.log('INFO', `[Worker] Granular Mode: Running ONLY ${targetCalc.name} for ${date}`);
|
|
106
|
-
} else {
|
|
107
|
-
logger.log('INFO', `[Worker] Bulk Mode: Running ${calcsInThisPass.length} calculations for ${date}`);
|
|
108
|
-
}
|
|
109
|
-
// ---------------------------
|
|
67
|
+
computationManifest = getManifest(config.activeProductLines || [], calculations, runDependencies);
|
|
68
|
+
} catch (manifestError) {
|
|
69
|
+
logger.log('FATAL', `[Worker] Failed to load Manifest: ${manifestError.message}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
110
72
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
73
|
+
// 5. EXECUTE (TRUSTED MODE)
|
|
74
|
+
// We do not check DB status or analyze feasibility. We assume Dispatcher did its job.
|
|
75
|
+
try {
|
|
76
|
+
logger.log('INFO', `[Worker] 📥 Received: ${computation} for ${date}`);
|
|
115
77
|
|
|
116
|
-
const result = await
|
|
78
|
+
const result = await executeDispatchTask(
|
|
117
79
|
date,
|
|
118
80
|
pass,
|
|
119
|
-
|
|
81
|
+
computation,
|
|
120
82
|
config,
|
|
121
|
-
runDependencies,
|
|
83
|
+
runDependencies,
|
|
122
84
|
computationManifest
|
|
123
85
|
);
|
|
124
86
|
|
|
125
|
-
if (result && result.updates
|
|
126
|
-
|
|
127
|
-
} else {
|
|
128
|
-
// In Granular Mode, this is common (e.g. if hash matched)
|
|
129
|
-
logger.log('INFO', `[Worker] Completed ${date} - No DB Writes (Up to date or skipped).`);
|
|
87
|
+
if (result && result.updates) {
|
|
88
|
+
logger.log('INFO', `[Worker] ✅ Stored: ${computation} for ${date}`);
|
|
130
89
|
}
|
|
131
|
-
|
|
90
|
+
|
|
132
91
|
} catch (err) {
|
|
133
|
-
logger.log('ERROR', `[Worker]
|
|
134
|
-
throw err; //
|
|
92
|
+
logger.log('ERROR', `[Worker] ❌ Failed: ${computation} for ${date}: ${err.message}`);
|
|
93
|
+
throw err; // Trigger Pub/Sub retry
|
|
135
94
|
}
|
|
136
95
|
}
|
|
137
96
|
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Build Reporter & Auto-Runner.
|
|
3
3
|
* Generates a "Pre-Flight" report of what the computation system WILL do.
|
|
4
|
-
*
|
|
5
|
-
* UPDATED: Implements Parallel Execution to prevent DEADLINE_EXCEEDED on 90-day scans.
|
|
4
|
+
* UPDATED: Removed "Smart Mocking" in favor of REAL data availability checks to detect gaps/impossible dates.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
const { analyzeDateExecution } = require('../WorkflowOrchestrator');
|
|
9
8
|
const { fetchComputationStatus } = require('../persistence/StatusRepository');
|
|
10
9
|
const { normalizeName, getExpectedDateStrings, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils');
|
|
10
|
+
const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
|
|
11
11
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
12
12
|
const pLimit = require('p-limit');
|
|
13
13
|
|
|
14
14
|
// Attempt to load package.json to get version. Path depends on where this is invoked.
|
|
15
|
-
let packageVersion = '1.0.
|
|
15
|
+
let packageVersion = '1.0.301'; // Bumped version to reflect logic change
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -78,7 +78,7 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
78
78
|
|
|
79
79
|
// 2. PARALLEL PROCESSING (Fix for DEADLINE_EXCEEDED)
|
|
80
80
|
// Run 20 reads in parallel.
|
|
81
|
-
//
|
|
81
|
+
// This is now slightly heavier because we verify root data existence, but necessary for accuracy.
|
|
82
82
|
const limit = pLimit(20);
|
|
83
83
|
|
|
84
84
|
const processingPromises = datesToCheck.map(dateStr => limit(async () => {
|
|
@@ -86,18 +86,21 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
86
86
|
// A. Fetch REAL status from DB (What ran previously?)
|
|
87
87
|
const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
|
|
88
88
|
|
|
89
|
-
// B.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
// B. REAL Root Data Check [FIXED]
|
|
90
|
+
// Previously we mocked this based on dates. Now we check if the data ACTUALLY exists.
|
|
91
|
+
// This ensures missing social data (even if after the start date) is flagged as IMPOSSIBLE.
|
|
92
|
+
const availability = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
|
|
93
|
+
|
|
94
|
+
const rootDataStatus = availability ? availability.status : {
|
|
95
|
+
hasPortfolio: false,
|
|
96
|
+
hasHistory: false,
|
|
97
|
+
hasSocial: false,
|
|
98
|
+
hasInsights: false,
|
|
99
|
+
hasPrices: false
|
|
97
100
|
};
|
|
98
101
|
|
|
99
102
|
// C. Run Logic Analysis
|
|
100
|
-
const analysis = analyzeDateExecution(dateStr, manifest,
|
|
103
|
+
const analysis = analyzeDateExecution(dateStr, manifest, rootDataStatus, dailyStatus, manifestMap);
|
|
101
104
|
|
|
102
105
|
// D. Format Findings
|
|
103
106
|
const dateSummary = {
|