bulltrackers-module 1.0.224 → 1.0.225
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,18 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Main Orchestrator. Coordinates the topological execution.
|
|
3
|
-
* UPDATED:
|
|
3
|
+
* UPDATED: Strict Dependency & Hash Cascade Logic with Explicit Failure Marking.
|
|
4
4
|
*/
|
|
5
|
-
const { normalizeName
|
|
5
|
+
const { normalizeName } = require('./utils/utils');
|
|
6
6
|
const { checkRootDataAvailability } = require('./data/AvailabilityChecker');
|
|
7
7
|
const { fetchExistingResults } = require('./data/DependencyFetcher');
|
|
8
8
|
const { fetchComputationStatus, updateComputationStatus } = require('./persistence/StatusRepository');
|
|
9
|
-
const { runBatchPriceComputation } = require('./executors/PriceBatchExecutor');
|
|
10
9
|
const { StandardExecutor } = require('./executors/StandardExecutor');
|
|
11
10
|
const { MetaExecutor } = require('./executors/MetaExecutor');
|
|
12
11
|
const { generateProcessId, PROCESS_TYPES } = require('./logger/logger');
|
|
13
12
|
|
|
14
|
-
const PARALLEL_BATCH_SIZE = 7;
|
|
15
|
-
|
|
16
13
|
function groupByPass(manifest) {
|
|
17
14
|
return manifest.reduce((acc, calc) => {
|
|
18
15
|
(acc[calc.pass] = acc[calc.pass] || []).push(calc);
|
|
@@ -21,22 +18,42 @@ function groupByPass(manifest) {
|
|
|
21
18
|
}
|
|
22
19
|
|
|
23
20
|
/**
|
|
24
|
-
* Performs
|
|
25
|
-
*
|
|
21
|
+
* Performs strict analysis of what can run based on availability and hash states.
|
|
22
|
+
* AIRTIGHT LOGIC:
|
|
23
|
+
* 1. Missing Root Data -> Blocked (Writes 'false' to DB)
|
|
24
|
+
* 2. Missing/Stale Dependency -> FailedDependency (Writes 'false' to DB)
|
|
25
|
+
* 3. Hash Mismatch -> ReRun (Cascade or Code Change)
|
|
26
|
+
* 4. No Result -> Run
|
|
27
|
+
* 5. Result Exists & Hash Match -> Skip
|
|
26
28
|
*/
|
|
27
29
|
function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus, manifestMap) {
|
|
28
30
|
const report = {
|
|
29
31
|
runnable: [],
|
|
30
|
-
blocked: [],
|
|
31
|
-
|
|
32
|
-
reRuns: []
|
|
32
|
+
blocked: [], // Missing Root Data
|
|
33
|
+
failedDependency: [], // Missing Dependency OR Stale Dependency
|
|
34
|
+
reRuns: [], // Hash Mismatch (Triggered by self code change OR upstream cascade)
|
|
35
|
+
skipped: [] // Already done & valid
|
|
33
36
|
};
|
|
34
37
|
|
|
35
|
-
// Helper: Is a dependency satisfied?
|
|
36
|
-
const isDepSatisfied = (depName, dailyStatus) => {
|
|
38
|
+
// Helper: Is a dependency satisfied AND valid (matching hash)?
|
|
39
|
+
const isDepSatisfied = (depName, dailyStatus, manifestMap) => {
|
|
37
40
|
const norm = normalizeName(depName);
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
const storedDepHash = dailyStatus[norm];
|
|
42
|
+
const depManifest = manifestMap.get(norm);
|
|
43
|
+
|
|
44
|
+
// 1. Must exist in DB. If missing, we cannot run.
|
|
45
|
+
if (!storedDepHash) return false;
|
|
46
|
+
|
|
47
|
+
// 2. Must exist in Manifest (Sanity check)
|
|
48
|
+
if (!depManifest) return false;
|
|
49
|
+
|
|
50
|
+
// 3. STRICT: The dependency's stored hash must match its current manifest hash.
|
|
51
|
+
// If 'A' changed code, 'A' has a new hash. If we are running 'B' (Pass 2),
|
|
52
|
+
// we expect 'A' (Pass 1) to have already run and updated the DB with the NEW hash.
|
|
53
|
+
// If DB still has OLD hash, 'A' failed or didn't run. 'B' is unsafe to run.
|
|
54
|
+
if (storedDepHash !== depManifest.hash) return false;
|
|
55
|
+
|
|
56
|
+
return true;
|
|
40
57
|
};
|
|
41
58
|
|
|
42
59
|
for (const calc of calcsInPass) {
|
|
@@ -44,7 +61,7 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
44
61
|
const storedHash = dailyStatus[cName];
|
|
45
62
|
const currentHash = calc.hash;
|
|
46
63
|
|
|
47
|
-
// 1. Root Data Check
|
|
64
|
+
// 1. Root Data Check (FATAL)
|
|
48
65
|
const missingRoots = [];
|
|
49
66
|
if (calc.rootDataDependencies) {
|
|
50
67
|
for (const dep of calc.rootDataDependencies) {
|
|
@@ -58,54 +75,41 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
58
75
|
|
|
59
76
|
if (missingRoots.length > 0) {
|
|
60
77
|
report.blocked.push({ name: cName, reason: `Missing Root Data: ${missingRoots.join(', ')}` });
|
|
61
|
-
continue;
|
|
78
|
+
continue; // Cannot proceed
|
|
62
79
|
}
|
|
63
80
|
|
|
64
|
-
// 2.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
report.reRuns.push({ name: cName, oldHash: storedHash, newHash: currentHash });
|
|
68
|
-
isReRun = true;
|
|
69
|
-
} else if (storedHash === true) {
|
|
70
|
-
// Legacy upgrade
|
|
71
|
-
report.reRuns.push({ name: cName, reason: 'Legacy Upgrade' });
|
|
72
|
-
isReRun = true;
|
|
73
|
-
} else if (storedHash && storedHash === currentHash) {
|
|
74
|
-
// Already done, and hashes match.
|
|
75
|
-
// Check if we need to run implies we ignore this?
|
|
76
|
-
// Usually we skip if done. But for "Analysis" log, we treat it as "Skipped/Done".
|
|
77
|
-
// If the user wants to FORCE run, that's different.
|
|
78
|
-
// Assuming standard flow: if done & hash match, we don't run.
|
|
79
|
-
// report.blocked.push({ name: cName, reason: 'Already up to date' });
|
|
80
|
-
// continue;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// 3. Dependency Check
|
|
81
|
+
// 2. Dependency Check (FATAL)
|
|
82
|
+
// Since we are in a topological pass system, all dependencies SHOULD be satisfied
|
|
83
|
+
// by previous passes. If not, it is a fatal error for this calculation.
|
|
84
84
|
const missingDeps = [];
|
|
85
85
|
if (calc.dependencies) {
|
|
86
86
|
for (const dep of calc.dependencies) {
|
|
87
|
-
|
|
88
|
-
// If it's a current pass dependency:
|
|
89
|
-
if (!isDepSatisfied(dep, dailyStatus)) {
|
|
87
|
+
if (!isDepSatisfied(dep, dailyStatus, manifestMap)) {
|
|
90
88
|
missingDeps.push(dep);
|
|
91
89
|
}
|
|
92
90
|
}
|
|
93
91
|
}
|
|
94
92
|
|
|
95
93
|
if (missingDeps.length > 0) {
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
//
|
|
94
|
+
report.failedDependency.push({ name: cName, missing: missingDeps });
|
|
95
|
+
continue; // Cannot proceed
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 3. Hash / State Check
|
|
99
|
+
if (!storedHash) {
|
|
100
|
+
// Case A: No result exists (or status is 'false' from previous failure) -> RUN
|
|
101
|
+
// Note: If storedHash is boolean false, !storedHash is true, so we retry.
|
|
102
|
+
report.runnable.push(calc);
|
|
103
|
+
} else if (storedHash !== currentHash) {
|
|
104
|
+
// Case B: Result exists, but hash mismatch -> RE-RUN
|
|
105
|
+
// This covers code changes in THIS calc, AND cascading changes from dependencies
|
|
106
|
+
report.reRuns.push({ name: cName, oldHash: storedHash, newHash: currentHash });
|
|
107
|
+
} else if (storedHash === true) {
|
|
108
|
+
// Case C: Legacy boolean status -> RE-RUN (Upgrade to hash)
|
|
109
|
+
report.reRuns.push({ name: cName, reason: 'Legacy Upgrade' });
|
|
104
110
|
} else {
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
report.runnable.push(calc);
|
|
108
|
-
}
|
|
111
|
+
// Case D: Result exists, Hash Matches -> SKIP
|
|
112
|
+
report.skipped.push({ name: cName });
|
|
109
113
|
}
|
|
110
114
|
}
|
|
111
115
|
|
|
@@ -124,7 +128,7 @@ async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, d
|
|
|
124
128
|
// 2. Check Data Availability (One shot)
|
|
125
129
|
const earliestDates = {
|
|
126
130
|
portfolio: new Date('2025-09-25T00:00:00Z'),
|
|
127
|
-
history: new Date('2025-11-05T00:00:00Z'),
|
|
131
|
+
history: new Date('2025-11-05T00:00:00Z'),
|
|
128
132
|
social: new Date('2025-10-30T00:00:00Z'),
|
|
129
133
|
insights: new Date('2025-08-26T00:00:00Z'),
|
|
130
134
|
price: new Date('2025-08-01T00:00:00Z')
|
|
@@ -133,39 +137,48 @@ async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, d
|
|
|
133
137
|
const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
|
|
134
138
|
const rootStatus = rootData ? rootData.status : { hasPortfolio: false, hasPrices: false, hasInsights: false, hasSocial: false, hasHistory: false };
|
|
135
139
|
|
|
136
|
-
// 3.
|
|
140
|
+
// 3. ANALYZE EXECUTION
|
|
137
141
|
const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
|
|
138
142
|
const analysisReport = analyzeDateExecution(dateStr, calcsInThisPass, rootStatus, dailyStatus, manifestMap);
|
|
139
143
|
|
|
140
|
-
// LOG
|
|
144
|
+
// 4. LOG ANALYSIS
|
|
141
145
|
logger.logDateAnalysis(dateStr, analysisReport);
|
|
142
146
|
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
147
|
+
// 5. MARK FAILURES (Explicitly write 'false' to DB for blocked items)
|
|
148
|
+
// This prevents UI/downstream consumers from waiting indefinitely.
|
|
149
|
+
const failureUpdates = {};
|
|
150
|
+
analysisReport.blocked.forEach(item => failureUpdates[item.name] = false);
|
|
151
|
+
analysisReport.failedDependency.forEach(item => failureUpdates[item.name] = false);
|
|
152
|
+
|
|
153
|
+
if (Object.keys(failureUpdates).length > 0) {
|
|
154
|
+
await updateComputationStatus(dateStr, failureUpdates, config, dependencies);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 6. EXECUTE RUNNABLES
|
|
146
158
|
const calcsToRunNames = new Set([
|
|
147
159
|
...analysisReport.runnable.map(c => c.name),
|
|
148
|
-
...analysisReport.reRuns.map(c => c.name)
|
|
160
|
+
...analysisReport.reRuns.map(c => c.name)
|
|
149
161
|
]);
|
|
150
162
|
|
|
151
163
|
const finalRunList = calcsInThisPass.filter(c => calcsToRunNames.has(normalizeName(c.name)));
|
|
152
164
|
|
|
153
165
|
if (!finalRunList.length) {
|
|
154
|
-
|
|
166
|
+
// Nothing to run (everything either skipped, blocked, or failed)
|
|
167
|
+
return { date: dateStr, updates: {}, skipped: analysisReport.skipped.length };
|
|
155
168
|
}
|
|
156
169
|
|
|
157
170
|
logger.log('INFO', `[Orchestrator] Executing ${finalRunList.length} calculations for ${dateStr}`, { processId: orchestratorPid });
|
|
158
171
|
|
|
159
|
-
// 5. Execution
|
|
160
172
|
const standardToRun = finalRunList.filter(c => c.type === 'standard');
|
|
161
173
|
const metaToRun = finalRunList.filter(c => c.type === 'meta');
|
|
162
174
|
|
|
163
175
|
const dateUpdates = {};
|
|
164
176
|
|
|
165
177
|
try {
|
|
166
|
-
const calcsRunning
|
|
178
|
+
const calcsRunning = [...standardToRun, ...metaToRun];
|
|
167
179
|
|
|
168
180
|
// Fetch dependencies (Previous pass results)
|
|
181
|
+
// includeSelf=false: We are re-running, so we ignore our own old results.
|
|
169
182
|
const existingResults = await fetchExistingResults(dateStr, calcsRunning, computationManifest, config, dependencies, false);
|
|
170
183
|
|
|
171
184
|
// Fetch Yesterday's results (if Historical)
|
|
@@ -183,15 +196,12 @@ async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, d
|
|
|
183
196
|
}
|
|
184
197
|
|
|
185
198
|
} catch (err) {
|
|
199
|
+
// If execution CRASHES (code bug, timeout), we log and throw.
|
|
200
|
+
// Pub/Sub will retry this message. This is correct for transient errors.
|
|
186
201
|
logger.log('ERROR', `[Orchestrator] Failed execution for ${dateStr}`, { processId: orchestratorPid, error: err.message });
|
|
187
202
|
throw err;
|
|
188
203
|
}
|
|
189
204
|
|
|
190
|
-
// 6. Status Update happens inside Executors, but we can log final success here
|
|
191
|
-
if (Object.keys(dateUpdates).length > 0) {
|
|
192
|
-
// await updateComputationStatus... (Handled by Executors currently)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
205
|
return { date: dateStr, updates: dateUpdates };
|
|
196
206
|
}
|
|
197
207
|
|