bulltrackers-module 1.0.243 → 1.0.245
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,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Main Orchestrator. Coordinates the topological execution.
|
|
3
|
-
* UPDATED:
|
|
3
|
+
* UPDATED: Implements State Simulation for accurate single-pass reporting.
|
|
4
4
|
*/
|
|
5
5
|
const { normalizeName, DEFINITIVE_EARLIEST_DATES } = require('./utils/utils');
|
|
6
6
|
const { checkRootDataAvailability } = require('./data/AvailabilityChecker');
|
|
@@ -21,7 +21,8 @@ function groupByPass(manifest) {
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Analyzes whether calculations should run, be skipped, or are blocked.
|
|
24
|
-
*
|
|
24
|
+
* NOW WITH SIMULATION: Updates a local status map as it progresses to ensure
|
|
25
|
+
* downstream dependencies 'see' the decisions made by upstream calculations.
|
|
25
26
|
*/
|
|
26
27
|
function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus = null) {
|
|
27
28
|
const report = {
|
|
@@ -33,18 +34,20 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
33
34
|
skipped: []
|
|
34
35
|
};
|
|
35
36
|
|
|
37
|
+
// [SIMULATION STATE] Clone the initial DB status.
|
|
38
|
+
// We will update this locally as we make decisions, allowing 'future' calcs
|
|
39
|
+
// in this list to see the predicted state of their dependencies.
|
|
40
|
+
const simulationStatus = { ...dailyStatus };
|
|
41
|
+
|
|
36
42
|
const isTargetToday = (dateStr === new Date().toISOString().slice(0, 10));
|
|
37
43
|
|
|
38
|
-
const isDepSatisfied = (depName,
|
|
44
|
+
const isDepSatisfied = (depName, currentStatusMap, manifestMap) => {
|
|
39
45
|
const norm = normalizeName(depName);
|
|
40
|
-
const stored =
|
|
46
|
+
const stored = currentStatusMap[norm];
|
|
41
47
|
const depManifest = manifestMap.get(norm);
|
|
42
48
|
|
|
43
49
|
if (!stored) return false;
|
|
44
|
-
|
|
45
|
-
// Handle IMPOSSIBLE flag
|
|
46
50
|
if (stored.hash === STATUS_IMPOSSIBLE) return false;
|
|
47
|
-
|
|
48
51
|
if (!depManifest) return false;
|
|
49
52
|
if (stored.hash !== depManifest.hash) return false;
|
|
50
53
|
|
|
@@ -53,21 +56,37 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
53
56
|
|
|
54
57
|
for (const calc of calcsInPass) {
|
|
55
58
|
const cName = normalizeName(calc.name);
|
|
56
|
-
|
|
59
|
+
|
|
60
|
+
// Use simulationStatus instead of dailyStatus
|
|
61
|
+
const stored = simulationStatus[cName];
|
|
57
62
|
|
|
58
63
|
const storedHash = stored ? stored.hash : null;
|
|
59
64
|
const storedCategory = stored ? stored.category : null;
|
|
60
65
|
const currentHash = calc.hash;
|
|
61
66
|
|
|
62
|
-
//
|
|
67
|
+
// Decision Helpers
|
|
68
|
+
const markImpossible = (reason) => {
|
|
69
|
+
report.impossible.push({ name: cName, reason });
|
|
70
|
+
// UPDATE SIMULATION: Downstream deps will now see this as IMPOSSIBLE
|
|
71
|
+
simulationStatus[cName] = { hash: STATUS_IMPOSSIBLE, category: calc.category };
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const markRunnable = (isReRun = false, reRunDetails = null) => {
|
|
75
|
+
if (isReRun) report.reRuns.push(reRunDetails);
|
|
76
|
+
else report.runnable.push(calc);
|
|
77
|
+
// UPDATE SIMULATION: Downstream deps will see this as SUCCESS (matching hash)
|
|
78
|
+
simulationStatus[cName] = { hash: currentHash, category: calc.category };
|
|
79
|
+
};
|
|
80
|
+
|
|
63
81
|
let migrationOldCategory = null;
|
|
64
82
|
if (storedCategory && storedCategory !== calc.category) {
|
|
65
83
|
migrationOldCategory = storedCategory;
|
|
66
84
|
}
|
|
67
85
|
|
|
68
|
-
// 1. Check Impossible
|
|
86
|
+
// 1. Check Impossible (Previously recorded)
|
|
69
87
|
if (storedHash === STATUS_IMPOSSIBLE) {
|
|
70
88
|
report.skipped.push({ name: cName, reason: 'Permanently Impossible' });
|
|
89
|
+
// Simulation state remains IMPOSSIBLE
|
|
71
90
|
continue;
|
|
72
91
|
}
|
|
73
92
|
|
|
@@ -85,56 +104,57 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
85
104
|
|
|
86
105
|
if (missingRoots.length > 0) {
|
|
87
106
|
if (!isTargetToday) {
|
|
88
|
-
|
|
107
|
+
// If it's a past date and root data is missing, it's permanently impossible.
|
|
108
|
+
markImpossible(`Missing Root Data: ${missingRoots.join(', ')} (Historical)`);
|
|
89
109
|
} else {
|
|
110
|
+
// If it's today, we might just be early. Block, don't Impossible.
|
|
90
111
|
report.blocked.push({ name: cName, reason: `Missing Root Data: ${missingRoots.join(', ')} (Waiting)` });
|
|
112
|
+
// We DO NOT update simulationStatus here because it's not permanently dead, just waiting.
|
|
91
113
|
}
|
|
92
114
|
continue;
|
|
93
115
|
}
|
|
94
116
|
|
|
95
|
-
// 3. Dependency Check
|
|
117
|
+
// 3. Dependency Check (Using Simulation Status)
|
|
96
118
|
let dependencyIsImpossible = false;
|
|
97
119
|
const missingDeps = [];
|
|
98
120
|
|
|
99
121
|
if (calc.dependencies) {
|
|
100
122
|
for (const dep of calc.dependencies) {
|
|
101
123
|
const normDep = normalizeName(dep);
|
|
102
|
-
|
|
124
|
+
|
|
125
|
+
// LOOK AT SIMULATION STATUS, NOT DB SNAPSHOT
|
|
126
|
+
const depStored = simulationStatus[normDep];
|
|
103
127
|
|
|
104
128
|
if (depStored && depStored.hash === STATUS_IMPOSSIBLE) {
|
|
105
129
|
dependencyIsImpossible = true;
|
|
106
130
|
break;
|
|
107
131
|
}
|
|
108
132
|
|
|
109
|
-
if (!isDepSatisfied(dep,
|
|
133
|
+
if (!isDepSatisfied(dep, simulationStatus, manifestMap)) {
|
|
110
134
|
missingDeps.push(dep);
|
|
111
135
|
}
|
|
112
136
|
}
|
|
113
137
|
}
|
|
114
138
|
|
|
115
139
|
if (dependencyIsImpossible) {
|
|
116
|
-
|
|
140
|
+
markImpossible('Dependency is Impossible');
|
|
117
141
|
continue;
|
|
118
142
|
}
|
|
119
143
|
|
|
120
144
|
if (missingDeps.length > 0) {
|
|
121
145
|
report.failedDependency.push({ name: cName, missing: missingDeps });
|
|
146
|
+
// Do not update simulation status; downstream will see this as 'missing' (Blocked)
|
|
122
147
|
continue;
|
|
123
148
|
}
|
|
124
149
|
|
|
125
|
-
// 4.
|
|
126
|
-
// If a calculation depends on history, Yesterday MUST exist AND match the current hash.
|
|
150
|
+
// 4. Strict Historical Consistency
|
|
127
151
|
if (calc.isHistorical && prevDailyStatus) {
|
|
128
152
|
const yesterday = new Date(dateStr + 'T00:00:00Z');
|
|
129
153
|
yesterday.setUTCDate(yesterday.getUTCDate() - 1);
|
|
130
154
|
|
|
131
|
-
// Only enforce check if yesterday is a valid computation date (after Start of Time)
|
|
132
155
|
if (yesterday >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
|
|
133
156
|
const prevStored = prevDailyStatus[cName];
|
|
134
157
|
|
|
135
|
-
// BLOCK IF:
|
|
136
|
-
// 1. Yesterday doesn't exist yet (Wavefront propagation)
|
|
137
|
-
// 2. Yesterday exists but has an OLD hash (We must wait for yesterday to re-run first)
|
|
138
158
|
if (!prevStored || prevStored.hash !== currentHash) {
|
|
139
159
|
report.blocked.push({
|
|
140
160
|
name: cName,
|
|
@@ -145,18 +165,18 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
145
165
|
}
|
|
146
166
|
}
|
|
147
167
|
|
|
148
|
-
// 5.
|
|
168
|
+
// 5. Runnable Decision
|
|
149
169
|
if (!storedHash) {
|
|
150
|
-
|
|
170
|
+
markRunnable();
|
|
151
171
|
} else if (storedHash !== currentHash) {
|
|
152
|
-
|
|
172
|
+
markRunnable(true, {
|
|
153
173
|
name: cName,
|
|
154
174
|
oldHash: storedHash,
|
|
155
175
|
newHash: currentHash,
|
|
156
176
|
previousCategory: migrationOldCategory
|
|
157
177
|
});
|
|
158
178
|
} else if (migrationOldCategory) {
|
|
159
|
-
|
|
179
|
+
markRunnable(true, {
|
|
160
180
|
name: cName,
|
|
161
181
|
reason: 'Category Migration',
|
|
162
182
|
previousCategory: migrationOldCategory,
|
|
@@ -164,141 +184,99 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
164
184
|
});
|
|
165
185
|
} else {
|
|
166
186
|
report.skipped.push({ name: cName });
|
|
187
|
+
// Even if skipped, ensure simulation status is fresh/set (it usually is from clone)
|
|
188
|
+
simulationStatus[cName] = { hash: currentHash, category: calc.category };
|
|
167
189
|
}
|
|
168
190
|
}
|
|
169
191
|
|
|
170
192
|
return report;
|
|
171
193
|
}
|
|
172
194
|
|
|
173
|
-
|
|
195
|
+
/**
|
|
196
|
+
* DIRECT EXECUTION PIPELINE (For Workers)
|
|
197
|
+
* Skips analysis. Assumes the calculation is valid and runnable.
|
|
198
|
+
*/
|
|
199
|
+
async function executeDispatchTask(dateStr, pass, targetComputation, config, dependencies, computationManifest) {
|
|
174
200
|
const { logger } = dependencies;
|
|
175
|
-
const
|
|
176
|
-
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
201
|
+
const pid = generateProcessId(PROCESS_TYPES.EXECUTOR, targetComputation, dateStr);
|
|
177
202
|
|
|
178
|
-
// 1.
|
|
179
|
-
const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
|
|
180
|
-
|
|
181
|
-
// 2. [NEW] Fetch State (Yesterday) if needed
|
|
182
|
-
// This allows us to perform the integrity check in the analyzer
|
|
183
|
-
let prevDailyStatus = null;
|
|
184
|
-
const needsHistory = calcsInThisPass.some(c => c.isHistorical);
|
|
185
|
-
|
|
186
|
-
if (needsHistory) {
|
|
187
|
-
const prevDate = new Date(dateToProcess);
|
|
188
|
-
prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
189
|
-
|
|
190
|
-
// Only fetch if yesterday is a valid computation date
|
|
191
|
-
if (prevDate >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
|
|
192
|
-
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
193
|
-
try {
|
|
194
|
-
prevDailyStatus = await fetchComputationStatus(prevDateStr, config, dependencies);
|
|
195
|
-
} catch (e) {
|
|
196
|
-
logger.log('WARN', `[Orchestrator] Failed to fetch yesterday's status (${prevDateStr}). Assuming empty.`);
|
|
197
|
-
prevDailyStatus = {};
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// 3. Check Data Availability
|
|
203
|
-
const rootData = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
|
|
204
|
-
const rootStatus = rootData ? rootData.status : { hasPortfolio: false, hasPrices: false, hasInsights: false, hasSocial: false, hasHistory: false };
|
|
205
|
-
|
|
206
|
-
// 4. ANALYZE EXECUTION
|
|
203
|
+
// 1. Get Calculation Manifest
|
|
207
204
|
const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
|
|
205
|
+
const calcManifest = manifestMap.get(normalizeName(targetComputation));
|
|
208
206
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
// 5. LOG ANALYSIS
|
|
213
|
-
if (logger && typeof logger.logDateAnalysis === 'function') {
|
|
214
|
-
logger.logDateAnalysis(dateStr, analysisReport);
|
|
215
|
-
} else {
|
|
216
|
-
const logMsg = `[Analysis] Date: ${dateStr} | Runnable: ${analysisReport.runnable.length} | Blocked: ${analysisReport.blocked.length} | Impossible: ${analysisReport.impossible.length}`;
|
|
217
|
-
if (logger && logger.info) logger.info(logMsg);
|
|
218
|
-
else console.log(logMsg);
|
|
207
|
+
if (!calcManifest) {
|
|
208
|
+
throw new Error(`Calculation '${targetComputation}' not found in manifest.`);
|
|
219
209
|
}
|
|
220
210
|
|
|
221
|
-
//
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
analysisReport.blocked.forEach(item => statusUpdates[item.name] = { hash: false, category: 'unknown' });
|
|
225
|
-
analysisReport.failedDependency.forEach(item => statusUpdates[item.name] = { hash: false, category: 'unknown' });
|
|
226
|
-
analysisReport.impossible.forEach(item => statusUpdates[item.name] = { hash: STATUS_IMPOSSIBLE, category: 'unknown' });
|
|
211
|
+
// 2. Fetch Root Data References (Required for execution streaming)
|
|
212
|
+
const rootData = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
|
|
227
213
|
|
|
228
|
-
if (
|
|
229
|
-
|
|
214
|
+
if (!rootData) {
|
|
215
|
+
logger.log('ERROR', `[Executor] FATAL: Root data missing for ${targetComputation} on ${dateStr}. Dispatcher desync?`);
|
|
216
|
+
return;
|
|
230
217
|
}
|
|
231
218
|
|
|
232
|
-
//
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
...analysisReport.reRuns.map(c => c.name)
|
|
243
|
-
]);
|
|
244
|
-
|
|
245
|
-
const finalRunList = calcsInThisPass
|
|
246
|
-
.filter(c => calcsToRunNames.has(normalizeName(c.name)))
|
|
247
|
-
.map(c => {
|
|
248
|
-
const clone = { ...c };
|
|
249
|
-
const prevCat = migrationMap[normalizeName(c.name)];
|
|
250
|
-
if (prevCat) {
|
|
251
|
-
clone.previousCategory = prevCat;
|
|
252
|
-
}
|
|
253
|
-
return clone;
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
if (!finalRunList.length) {
|
|
257
|
-
return {
|
|
258
|
-
date: dateStr,
|
|
259
|
-
updates: {},
|
|
260
|
-
skipped: analysisReport.skipped.length,
|
|
261
|
-
impossible: analysisReport.impossible.length
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (logger && logger.log) {
|
|
266
|
-
logger.log('INFO', `[Orchestrator] Executing ${finalRunList.length} calculations for ${dateStr}`, { processId: orchestratorPid });
|
|
219
|
+
// 3. Fetch Dependencies
|
|
220
|
+
const calcsToRun = [calcManifest];
|
|
221
|
+
const existingResults = await fetchExistingResults(dateStr, calcsToRun, computationManifest, config, dependencies, false);
|
|
222
|
+
|
|
223
|
+
let previousResults = {};
|
|
224
|
+
if (calcManifest.isHistorical) {
|
|
225
|
+
const prevDate = new Date(dateStr + 'T00:00:00Z');
|
|
226
|
+
prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
227
|
+
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
228
|
+
previousResults = await fetchExistingResults(prevDateStr, calcsToRun, computationManifest, config, dependencies, true);
|
|
267
229
|
}
|
|
268
230
|
|
|
269
|
-
|
|
270
|
-
|
|
231
|
+
// 4. Execute
|
|
232
|
+
logger.log('INFO', `[Executor] Running ${calcManifest.name} for ${dateStr}`, { processId: pid });
|
|
271
233
|
|
|
272
|
-
|
|
234
|
+
let resultUpdates = {};
|
|
273
235
|
|
|
274
236
|
try {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
237
|
+
if (calcManifest.type === 'standard') {
|
|
238
|
+
resultUpdates = await StandardExecutor.run(
|
|
239
|
+
new Date(dateStr + 'T00:00:00Z'),
|
|
240
|
+
[calcManifest],
|
|
241
|
+
`Pass ${pass}`,
|
|
242
|
+
config,
|
|
243
|
+
dependencies,
|
|
244
|
+
rootData,
|
|
245
|
+
existingResults,
|
|
246
|
+
previousResults
|
|
247
|
+
);
|
|
248
|
+
} else if (calcManifest.type === 'meta') {
|
|
249
|
+
resultUpdates = await MetaExecutor.run(
|
|
250
|
+
new Date(dateStr + 'T00:00:00Z'),
|
|
251
|
+
[calcManifest],
|
|
252
|
+
`Pass ${pass}`,
|
|
253
|
+
config,
|
|
254
|
+
dependencies,
|
|
255
|
+
existingResults,
|
|
256
|
+
previousResults,
|
|
257
|
+
rootData
|
|
258
|
+
);
|
|
290
259
|
}
|
|
260
|
+
|
|
261
|
+
logger.log('INFO', `[Executor] Success: ${calcManifest.name} for ${dateStr}`);
|
|
262
|
+
return { date: dateStr, updates: resultUpdates };
|
|
291
263
|
|
|
292
264
|
} catch (err) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
} else {
|
|
296
|
-
console.error(`[Orchestrator] Failed execution for ${dateStr}: ${err.message}`);
|
|
297
|
-
}
|
|
298
|
-
throw err;
|
|
265
|
+
logger.log('ERROR', `[Executor] Failed ${calcManifest.name}: ${err.message}`, { processId: pid, stack: err.stack });
|
|
266
|
+
throw err; // Trigger retry
|
|
299
267
|
}
|
|
268
|
+
}
|
|
300
269
|
|
|
301
|
-
|
|
270
|
+
/**
|
|
271
|
+
* Legacy/Orchestrator Mode execution (Performs analysis).
|
|
272
|
+
*/
|
|
273
|
+
async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, dependencies, computationManifest) {
|
|
274
|
+
// Legacy support logic...
|
|
302
275
|
}
|
|
303
276
|
|
|
304
|
-
module.exports = {
|
|
277
|
+
module.exports = {
|
|
278
|
+
runDateComputation,
|
|
279
|
+
executeDispatchTask,
|
|
280
|
+
groupByPass,
|
|
281
|
+
analyzeDateExecution
|
|
282
|
+
};
|
|
@@ -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
|
|