bulltrackers-module 1.0.223 → 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.
- package/functions/computation-system/WorkflowOrchestrator.js +146 -90
- package/functions/computation-system/context/ManifestBuilder.js +7 -8
- package/functions/computation-system/controllers/computation_controller.js +0 -4
- package/functions/computation-system/data/AvailabilityChecker.js +4 -15
- package/functions/computation-system/helpers/computation_worker.js +51 -12
- package/functions/computation-system/logger/logger.js +454 -0
- package/functions/computation-system/persistence/ResultCommitter.js +23 -20
- package/functions/computation-system/topology/ManifestLoader.js +43 -0
- package/package.json +1 -1
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Main Orchestrator. Coordinates the topological execution
|
|
2
|
+
* @fileoverview Main Orchestrator. Coordinates the topological execution.
|
|
3
|
+
* UPDATED: Strict Dependency & Hash Cascade Logic with Explicit Failure Marking.
|
|
3
4
|
*/
|
|
4
|
-
const { normalizeName
|
|
5
|
-
const { checkRootDataAvailability
|
|
5
|
+
const { normalizeName } = require('./utils/utils');
|
|
6
|
+
const { checkRootDataAvailability } = require('./data/AvailabilityChecker');
|
|
6
7
|
const { fetchExistingResults } = require('./data/DependencyFetcher');
|
|
7
8
|
const { fetchComputationStatus, updateComputationStatus } = require('./persistence/StatusRepository');
|
|
8
|
-
const { runBatchPriceComputation } = require('./executors/PriceBatchExecutor');
|
|
9
9
|
const { StandardExecutor } = require('./executors/StandardExecutor');
|
|
10
10
|
const { MetaExecutor } = require('./executors/MetaExecutor');
|
|
11
|
-
|
|
12
|
-
const PARALLEL_BATCH_SIZE = 7;
|
|
11
|
+
const { generateProcessId, PROCESS_TYPES } = require('./logger/logger');
|
|
13
12
|
|
|
14
13
|
function groupByPass(manifest) {
|
|
15
14
|
return manifest.reduce((acc, calc) => {
|
|
@@ -18,135 +17,192 @@ function groupByPass(manifest) {
|
|
|
18
17
|
}, {});
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
/**
|
|
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
|
|
28
|
+
*/
|
|
29
|
+
function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus, manifestMap) {
|
|
30
|
+
const report = {
|
|
31
|
+
runnable: [],
|
|
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
|
|
36
|
+
};
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
// Helper: Is a dependency satisfied AND valid (matching hash)?
|
|
39
|
+
const isDepSatisfied = (depName, dailyStatus, manifestMap) => {
|
|
40
|
+
const norm = normalizeName(depName);
|
|
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;
|
|
34
57
|
};
|
|
35
|
-
earliestDates.absoluteEarliest = Object.values(earliestDates).reduce((a, b) => a < b ? a : b);
|
|
36
|
-
const passes = groupByPass(computationManifest);
|
|
37
|
-
const calcsInThisPass = passes[passToRun] || [];
|
|
38
58
|
|
|
39
|
-
|
|
59
|
+
for (const calc of calcsInPass) {
|
|
60
|
+
const cName = normalizeName(calc.name);
|
|
61
|
+
const storedHash = dailyStatus[cName];
|
|
62
|
+
const currentHash = calc.hash;
|
|
40
63
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
await runBatchPriceComputation(config, dependencies, allExpectedDates, priceBatchCalcs);
|
|
52
|
-
} catch (e) {
|
|
53
|
-
logger.log('ERROR', 'Batch Price failed', e);
|
|
64
|
+
// 1. Root Data Check (FATAL)
|
|
65
|
+
const missingRoots = [];
|
|
66
|
+
if (calc.rootDataDependencies) {
|
|
67
|
+
for (const dep of calc.rootDataDependencies) {
|
|
68
|
+
if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missingRoots.push('portfolio');
|
|
69
|
+
if (dep === 'insights' && !rootDataStatus.hasInsights) missingRoots.push('insights');
|
|
70
|
+
if (dep === 'social' && !rootDataStatus.hasSocial) missingRoots.push('social');
|
|
71
|
+
if (dep === 'history' && !rootDataStatus.hasHistory) missingRoots.push('history');
|
|
72
|
+
if (dep === 'price' && !rootDataStatus.hasPrices) missingRoots.push('price');
|
|
73
|
+
}
|
|
54
74
|
}
|
|
55
|
-
}
|
|
56
75
|
|
|
57
|
-
|
|
76
|
+
if (missingRoots.length > 0) {
|
|
77
|
+
report.blocked.push({ name: cName, reason: `Missing Root Data: ${missingRoots.join(', ')}` });
|
|
78
|
+
continue; // Cannot proceed
|
|
79
|
+
}
|
|
58
80
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
)
|
|
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
|
+
const missingDeps = [];
|
|
85
|
+
if (calc.dependencies) {
|
|
86
|
+
for (const dep of calc.dependencies) {
|
|
87
|
+
if (!isDepSatisfied(dep, dailyStatus, manifestMap)) {
|
|
88
|
+
missingDeps.push(dep);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (missingDeps.length > 0) {
|
|
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' });
|
|
110
|
+
} else {
|
|
111
|
+
// Case D: Result exists, Hash Matches -> SKIP
|
|
112
|
+
report.skipped.push({ name: cName });
|
|
113
|
+
}
|
|
64
114
|
}
|
|
115
|
+
|
|
116
|
+
return report;
|
|
65
117
|
}
|
|
66
118
|
|
|
67
119
|
async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, dependencies, computationManifest) {
|
|
68
120
|
const { logger } = dependencies;
|
|
121
|
+
const orchestratorPid = generateProcessId(PROCESS_TYPES.ORCHESTRATOR, passToRun, dateStr);
|
|
122
|
+
|
|
69
123
|
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
70
124
|
|
|
71
|
-
// 1.
|
|
72
|
-
const dailyStatus
|
|
73
|
-
const calcsToAttempt = [];
|
|
125
|
+
// 1. Fetch State
|
|
126
|
+
const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
|
|
74
127
|
|
|
75
|
-
|
|
76
|
-
const cName = normalizeName(calc.name);
|
|
77
|
-
const storedStatus = dailyStatus[cName];
|
|
78
|
-
const currentHash = calc.hash;
|
|
79
|
-
|
|
80
|
-
if (!storedStatus) {
|
|
81
|
-
calcsToAttempt.push(calc); continue;
|
|
82
|
-
}
|
|
83
|
-
if (typeof storedStatus === 'string' && currentHash && storedStatus !== currentHash) {
|
|
84
|
-
logger.log('INFO', `[Versioning] ${cName}: Code Changed.`);
|
|
85
|
-
calcsToAttempt.push(calc); continue;
|
|
86
|
-
}
|
|
87
|
-
if (storedStatus === true && currentHash) {
|
|
88
|
-
logger.log('INFO', `[Versioning] ${cName}: Upgrading legacy status.`);
|
|
89
|
-
calcsToAttempt.push(calc); continue;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (!calcsToAttempt.length) return null;
|
|
94
|
-
|
|
95
|
-
// 2. Data Availability Check
|
|
128
|
+
// 2. Check Data Availability (One shot)
|
|
96
129
|
const earliestDates = {
|
|
97
130
|
portfolio: new Date('2025-09-25T00:00:00Z'),
|
|
98
|
-
history: new Date('2025-11-05T00:00:00Z'),
|
|
131
|
+
history: new Date('2025-11-05T00:00:00Z'),
|
|
99
132
|
social: new Date('2025-10-30T00:00:00Z'),
|
|
100
133
|
insights: new Date('2025-08-26T00:00:00Z'),
|
|
101
134
|
price: new Date('2025-08-01T00:00:00Z')
|
|
102
135
|
};
|
|
103
|
-
|
|
136
|
+
|
|
104
137
|
const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
|
|
105
|
-
|
|
138
|
+
const rootStatus = rootData ? rootData.status : { hasPortfolio: false, hasPrices: false, hasInsights: false, hasSocial: false, hasHistory: false };
|
|
139
|
+
|
|
140
|
+
// 3. ANALYZE EXECUTION
|
|
141
|
+
const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
|
|
142
|
+
const analysisReport = analyzeDateExecution(dateStr, calcsInThisPass, rootStatus, dailyStatus, manifestMap);
|
|
143
|
+
|
|
144
|
+
// 4. LOG ANALYSIS
|
|
145
|
+
logger.logDateAnalysis(dateStr, analysisReport);
|
|
146
|
+
|
|
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);
|
|
106
152
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
153
|
+
if (Object.keys(failureUpdates).length > 0) {
|
|
154
|
+
await updateComputationStatus(dateStr, failureUpdates, config, dependencies);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 6. EXECUTE RUNNABLES
|
|
158
|
+
const calcsToRunNames = new Set([
|
|
159
|
+
...analysisReport.runnable.map(c => c.name),
|
|
160
|
+
...analysisReport.reRuns.map(c => c.name)
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
const finalRunList = calcsInThisPass.filter(c => calcsToRunNames.has(normalizeName(c.name)));
|
|
164
|
+
|
|
165
|
+
if (!finalRunList.length) {
|
|
166
|
+
// Nothing to run (everything either skipped, blocked, or failed)
|
|
167
|
+
return { date: dateStr, updates: {}, skipped: analysisReport.skipped.length };
|
|
115
168
|
}
|
|
116
169
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
170
|
+
logger.log('INFO', `[Orchestrator] Executing ${finalRunList.length} calculations for ${dateStr}`, { processId: orchestratorPid });
|
|
171
|
+
|
|
172
|
+
const standardToRun = finalRunList.filter(c => c.type === 'standard');
|
|
173
|
+
const metaToRun = finalRunList.filter(c => c.type === 'meta');
|
|
120
174
|
|
|
121
175
|
const dateUpdates = {};
|
|
122
176
|
|
|
123
177
|
try {
|
|
124
|
-
const calcsRunning
|
|
178
|
+
const calcsRunning = [...standardToRun, ...metaToRun];
|
|
179
|
+
|
|
180
|
+
// Fetch dependencies (Previous pass results)
|
|
181
|
+
// includeSelf=false: We are re-running, so we ignore our own old results.
|
|
125
182
|
const existingResults = await fetchExistingResults(dateStr, calcsRunning, computationManifest, config, dependencies, false);
|
|
183
|
+
|
|
184
|
+
// Fetch Yesterday's results (if Historical)
|
|
126
185
|
const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
127
186
|
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
128
187
|
const previousResults = await fetchExistingResults(prevDateStr, calcsRunning, computationManifest, config, dependencies, true);
|
|
129
188
|
|
|
130
189
|
if (standardToRun.length) {
|
|
131
|
-
const updates = await StandardExecutor.run(dateToProcess, standardToRun, `Pass ${passToRun}
|
|
190
|
+
const updates = await StandardExecutor.run(dateToProcess, standardToRun, `Pass ${passToRun}`, config, dependencies, rootData, existingResults, previousResults, false);
|
|
132
191
|
Object.assign(dateUpdates, updates);
|
|
133
192
|
}
|
|
134
193
|
if (metaToRun.length) {
|
|
135
|
-
const updates = await MetaExecutor.run(dateToProcess, metaToRun, `Pass ${passToRun}
|
|
194
|
+
const updates = await MetaExecutor.run(dateToProcess, metaToRun, `Pass ${passToRun}`, config, dependencies, existingResults, previousResults, rootData, false);
|
|
136
195
|
Object.assign(dateUpdates, updates);
|
|
137
196
|
}
|
|
138
197
|
|
|
139
198
|
} catch (err) {
|
|
140
|
-
|
|
141
|
-
|
|
199
|
+
// If execution CRASHES (code bug, timeout), we log and throw.
|
|
200
|
+
// Pub/Sub will retry this message. This is correct for transient errors.
|
|
201
|
+
logger.log('ERROR', `[Orchestrator] Failed execution for ${dateStr}`, { processId: orchestratorPid, error: err.message });
|
|
142
202
|
throw err;
|
|
143
203
|
}
|
|
144
204
|
|
|
145
|
-
if (Object.keys(dateUpdates).length > 0) {
|
|
146
|
-
await updateComputationStatus(dateStr, dateUpdates, config, dependencies);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
205
|
return { date: dateStr, updates: dateUpdates };
|
|
150
206
|
}
|
|
151
207
|
|
|
152
|
-
module.exports = {
|
|
208
|
+
module.exports = { runDateComputation, groupByPass };
|
|
@@ -50,18 +50,17 @@ function buildDynamicTriggers() {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
const LAYER_HASHES = {};
|
|
53
|
-
for (const [name, exports] of Object.entries(LAYER_GROUPS)) {
|
|
54
|
-
|
|
55
|
-
}
|
|
53
|
+
for (const [name, exports] of Object.entries(LAYER_GROUPS)) { LAYER_HASHES[name] = generateLayerHashes(exports, name); }
|
|
54
|
+
|
|
56
55
|
const LAYER_TRIGGERS = buildDynamicTriggers();
|
|
57
56
|
|
|
58
57
|
const log = {
|
|
59
|
-
info:
|
|
60
|
-
step:
|
|
61
|
-
warn:
|
|
58
|
+
info: (msg) => console.log('ℹ︎ ' + msg),
|
|
59
|
+
step: (msg) => console.log('› ' + msg),
|
|
60
|
+
warn: (msg) => console.warn('⚠︎ ' + msg),
|
|
62
61
|
success: (msg) => console.log('✔︎ ' + msg),
|
|
63
|
-
error:
|
|
64
|
-
fatal:
|
|
62
|
+
error: (msg) => console.error('✖ ' + msg),
|
|
63
|
+
fatal: (msg) => { console.error('✖ FATAL ✖ ' + msg); console.error('✖ FATAL ✖ Manifest build FAILED.'); },
|
|
65
64
|
divider: (label) => { const line = ''.padEnd(60, '─'); console.log(`\n${line}\n${label}\n${line}\n`); },
|
|
66
65
|
};
|
|
67
66
|
|
|
@@ -22,7 +22,6 @@ const LEGACY_MAPPING = {
|
|
|
22
22
|
priceExtractor: 'priceExtractor',
|
|
23
23
|
InsightsExtractor: 'insights',
|
|
24
24
|
UserClassifier: 'classifier',
|
|
25
|
-
// Added based on your profiling.js file:
|
|
26
25
|
Psychometrics: 'psychometrics',
|
|
27
26
|
CognitiveBiases: 'bias',
|
|
28
27
|
SkillAttribution: 'skill',
|
|
@@ -47,7 +46,6 @@ class ContextBuilder {
|
|
|
47
46
|
for (const [key, value] of Object.entries(mathLayer)) { mathContext[key] = value; const legacyKey = LEGACY_MAPPING[key]; if (legacyKey) { mathContext[legacyKey] = value; } }
|
|
48
47
|
return mathContext;
|
|
49
48
|
}
|
|
50
|
-
// ... (rest of class remains identical)
|
|
51
49
|
static buildPerUserContext(options) {
|
|
52
50
|
const { todayPortfolio, yesterdayPortfolio, todayHistory, yesterdayHistory, userId, userType, dateStr, metadata, mappings, insights, socialData, computedDependencies, previousComputedDependencies, config, deps } = options;
|
|
53
51
|
return {
|
|
@@ -80,7 +78,6 @@ class ContextBuilder {
|
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
class ComputationExecutor {
|
|
83
|
-
// ... (remains identical to previous version)
|
|
84
81
|
constructor(config, dependencies, dataLoader) {
|
|
85
82
|
this.config = config;
|
|
86
83
|
this.deps = dependencies;
|
|
@@ -146,5 +143,4 @@ class ComputationController {
|
|
|
146
143
|
}
|
|
147
144
|
}
|
|
148
145
|
|
|
149
|
-
// EXPORT LEGACY_MAPPING SO BUILDER CAN USE IT
|
|
150
146
|
module.exports = { ComputationController, LEGACY_MAPPING };
|
|
@@ -52,29 +52,18 @@ function getViableCalculations(candidates, fullManifest, rootDataStatus, dailySt
|
|
|
52
52
|
const depManifest = manifestMap.get(normDep);
|
|
53
53
|
|
|
54
54
|
// If dependency is missing from manifest, we can't verify it (shouldn't happen)
|
|
55
|
-
if (!depManifest) {
|
|
56
|
-
dependenciesMet = false;
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
55
|
+
if (!depManifest) { dependenciesMet = false; break; }
|
|
59
56
|
|
|
60
57
|
// CHECK: Does the dependency exist in DB?
|
|
61
|
-
if (!storedHash) {
|
|
62
|
-
dependenciesMet = false;
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
58
|
+
if (!storedHash) { dependenciesMet = false; break; }
|
|
65
59
|
|
|
66
60
|
// CHECK: Does the stored hash match the current code hash?
|
|
67
61
|
// This prevents running on stale data if a dependency failed to update.
|
|
68
|
-
if (storedHash !== depManifest.hash) {
|
|
69
|
-
dependenciesMet = false;
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
62
|
+
if (storedHash !== depManifest.hash) { dependenciesMet = false; break; }
|
|
72
63
|
}
|
|
73
64
|
}
|
|
74
65
|
|
|
75
|
-
if (dependenciesMet) {
|
|
76
|
-
viable.push(calc);
|
|
77
|
-
}
|
|
66
|
+
if (dependenciesMet) { viable.push(calc); }
|
|
78
67
|
}
|
|
79
68
|
|
|
80
69
|
return viable;
|
|
@@ -1,25 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FILENAME:
|
|
2
|
+
* FILENAME: computation-system/helpers/computation_worker.js
|
|
3
3
|
* PURPOSE: Consumes computation tasks from Pub/Sub and executes them.
|
|
4
|
-
*
|
|
4
|
+
* UPDATED: Correctly points to layers/index.js and wraps it for the ManifestBuilder.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { runDateComputation, groupByPass } = require('../WorkflowOrchestrator.js');
|
|
8
|
+
const { getManifest } = require('../topology/ManifestLoader');
|
|
9
|
+
|
|
10
|
+
// 1. IMPORT CALCULATIONS
|
|
11
|
+
// User confirmation: Calculations are barrel-loaded into layers/index.js
|
|
12
|
+
const rawLayers = require('../layers/index');
|
|
13
|
+
|
|
14
|
+
// 2. PREPARE FOR MANIFEST
|
|
15
|
+
// The ManifestBuilder expects a structure like: { packageName: { CalculationClass, ... } }
|
|
16
|
+
// Since layers/index.js returns a flat object, we wrap it in a 'core' group here.
|
|
17
|
+
const calculations = {
|
|
18
|
+
core: rawLayers
|
|
19
|
+
};
|
|
8
20
|
|
|
9
21
|
/**
|
|
10
22
|
* Handles a single Pub/Sub message for a computation task.
|
|
11
23
|
* Supports both Gen 1 (Message) and Gen 2 (CloudEvent) formats.
|
|
24
|
+
* * @param {object} message - The Pub/Sub message payload.
|
|
25
|
+
* @param {object} config - System configuration (must include activeProductLines).
|
|
26
|
+
* @param {object} dependencies - System dependencies (logger, db, etc.).
|
|
12
27
|
*/
|
|
13
|
-
async function handleComputationTask(message, config, dependencies
|
|
28
|
+
async function handleComputationTask(message, config, dependencies) {
|
|
14
29
|
const { logger } = dependencies;
|
|
15
30
|
|
|
31
|
+
// 3. LAZY LOAD MANIFEST
|
|
32
|
+
// This ensures we only build the manifest when we actually receive a task.
|
|
33
|
+
// We pass our wrapped 'calculations' object here.
|
|
34
|
+
let computationManifest;
|
|
35
|
+
try {
|
|
36
|
+
computationManifest = getManifest(
|
|
37
|
+
config.activeProductLines || [],
|
|
38
|
+
calculations,
|
|
39
|
+
dependencies
|
|
40
|
+
);
|
|
41
|
+
} catch (manifestError) {
|
|
42
|
+
logger.log('FATAL', `[Worker] Failed to load Manifest: ${manifestError.message}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 4. PARSE PUB/SUB MESSAGE
|
|
16
47
|
let data;
|
|
17
48
|
try {
|
|
18
|
-
// 1. Handle Cloud Functions Gen 2 (CloudEvent) -> Gen 1 -> Direct JSON -> Message
|
|
19
49
|
if (message.data && message.data.message && message.data.message.data) {
|
|
20
|
-
const buffer = Buffer.from(message.data.message.data, 'base64');
|
|
50
|
+
const buffer = Buffer.from(message.data.message.data, 'base64');
|
|
51
|
+
data = JSON.parse(buffer.toString());
|
|
21
52
|
} else if (message.data && typeof message.data === 'string') {
|
|
22
|
-
const buffer = Buffer.from(message.data, 'base64');
|
|
53
|
+
const buffer = Buffer.from(message.data, 'base64');
|
|
54
|
+
data = JSON.parse(buffer.toString());
|
|
23
55
|
} else if (message.json) {
|
|
24
56
|
data = message.json;
|
|
25
57
|
} else {
|
|
@@ -30,8 +62,8 @@ async function handleComputationTask(message, config, dependencies, computationM
|
|
|
30
62
|
return;
|
|
31
63
|
}
|
|
32
64
|
|
|
65
|
+
// 5. EXECUTE TASK
|
|
33
66
|
try {
|
|
34
|
-
// Validate Action
|
|
35
67
|
if (!data || data.action !== 'RUN_COMPUTATION_DATE') {
|
|
36
68
|
if (data) logger.log('WARN', `[Worker] Unknown or missing action: ${data?.action}. Ignoring.`);
|
|
37
69
|
return;
|
|
@@ -53,17 +85,24 @@ async function handleComputationTask(message, config, dependencies, computationM
|
|
|
53
85
|
return;
|
|
54
86
|
}
|
|
55
87
|
|
|
56
|
-
const result = await runDateComputation(
|
|
88
|
+
const result = await runDateComputation(
|
|
89
|
+
date,
|
|
90
|
+
pass,
|
|
91
|
+
calcsInThisPass,
|
|
92
|
+
config,
|
|
93
|
+
dependencies,
|
|
94
|
+
computationManifest
|
|
95
|
+
);
|
|
57
96
|
|
|
58
|
-
if (result) {
|
|
59
|
-
logger.log('INFO', `[Worker] Successfully processed ${date} (Pass ${pass}). Updates: ${Object.keys(result.updates
|
|
97
|
+
if (result && result.updates && Object.keys(result.updates).length > 0) {
|
|
98
|
+
logger.log('INFO', `[Worker] Successfully processed ${date} (Pass ${pass}). Updates: ${Object.keys(result.updates).length}`);
|
|
60
99
|
} else {
|
|
61
|
-
logger.log('INFO', `[Worker] Processed ${date} (Pass ${pass}) -
|
|
100
|
+
logger.log('INFO', `[Worker] Processed ${date} (Pass ${pass}) - No updates.`);
|
|
62
101
|
}
|
|
63
102
|
|
|
64
103
|
} catch (err) {
|
|
65
104
|
logger.log('ERROR', `[Worker] Fatal error processing task: ${err.message}`, { stack: err.stack });
|
|
66
|
-
throw err;
|
|
105
|
+
throw err;
|
|
67
106
|
}
|
|
68
107
|
}
|
|
69
108
|
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Structured Logging System for Computation Engine
|
|
3
|
+
* Provides comprehensive tracking with process IDs, context, and filtering capabilities.
|
|
4
|
+
* UPDATED: Includes Pre-flight Date Analysis and Storage Observability.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Log Levels (Ordered by Severity)
|
|
11
|
+
*/
|
|
12
|
+
const LOG_LEVELS = {
|
|
13
|
+
TRACE: 0,
|
|
14
|
+
DEBUG: 1,
|
|
15
|
+
INFO: 2,
|
|
16
|
+
WARN: 3,
|
|
17
|
+
ERROR: 4,
|
|
18
|
+
FATAL: 5
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Process Types for Tracking
|
|
23
|
+
*/
|
|
24
|
+
const PROCESS_TYPES = {
|
|
25
|
+
MANIFEST: 'manifest',
|
|
26
|
+
ORCHESTRATOR: 'orchestrator',
|
|
27
|
+
EXECUTOR: 'executor',
|
|
28
|
+
STORAGE: 'storage',
|
|
29
|
+
WORKER: 'worker',
|
|
30
|
+
ANALYSIS: 'analysis',
|
|
31
|
+
// Legacy / Specific aliases
|
|
32
|
+
SCHEMA_GENERATION: 'schema_generation',
|
|
33
|
+
COMPUTATION_EXECUTION: 'computation_execution',
|
|
34
|
+
DEPENDENCY_FETCH: 'dependency_fetch',
|
|
35
|
+
DATA_AVAILABILITY: 'data_availability',
|
|
36
|
+
DISPATCH: 'dispatch'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generates a deterministic process ID from inputs.
|
|
41
|
+
* Ensures that logs for the same computation/date always have the same Trace ID.
|
|
42
|
+
* @param {string} type - Process type (e.g., 'orchestrator')
|
|
43
|
+
* @param {string} identifier - Unique key (e.g., pass name, calculation name)
|
|
44
|
+
* @param {string} date - Date string YYYY-MM-DD (optional)
|
|
45
|
+
* @returns {string} 16-character hexadecimal process ID
|
|
46
|
+
*/
|
|
47
|
+
function generateProcessId(type, identifier, date = '') {
|
|
48
|
+
const input = `${type}|${identifier}|${date}`;
|
|
49
|
+
return crypto.createHash('sha256').update(input).digest('hex').substring(0, 16);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Formats a log entry into structured JSON
|
|
54
|
+
*/
|
|
55
|
+
function formatLogEntry(entry) {
|
|
56
|
+
return JSON.stringify({
|
|
57
|
+
timestamp: entry.timestamp,
|
|
58
|
+
level: entry.level,
|
|
59
|
+
processType: entry.processType,
|
|
60
|
+
processId: entry.processId,
|
|
61
|
+
computationName: entry.computationName,
|
|
62
|
+
date: entry.date,
|
|
63
|
+
message: entry.message,
|
|
64
|
+
context: entry.context,
|
|
65
|
+
metadata: entry.metadata,
|
|
66
|
+
// Specific fields for specialized logs
|
|
67
|
+
stats: entry.stats,
|
|
68
|
+
storage: entry.storage,
|
|
69
|
+
details: entry.details
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Main Structured Logger Class
|
|
75
|
+
*/
|
|
76
|
+
class StructuredLogger {
|
|
77
|
+
constructor(config = {}) {
|
|
78
|
+
this.config = {
|
|
79
|
+
minLevel: config.minLevel || LOG_LEVELS.INFO,
|
|
80
|
+
enableConsole: config.enableConsole !== false,
|
|
81
|
+
enableStructured: config.enableStructured !== false,
|
|
82
|
+
includeStackTrace: config.includeStackTrace !== false,
|
|
83
|
+
...config
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
this.activeProcesses = new Map();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Starts a tracked process and returns a ProcessLogger
|
|
91
|
+
* @param {string} processType - Type from PROCESS_TYPES
|
|
92
|
+
* @param {string} computationName - Optional computation name
|
|
93
|
+
* @param {string} date - Optional date string
|
|
94
|
+
* @returns {ProcessLogger}
|
|
95
|
+
*/
|
|
96
|
+
startProcess(processType, computationName = null, date = null) {
|
|
97
|
+
// Use deterministic ID if components are present, else random/time-based fallback
|
|
98
|
+
const processId = (computationName || date)
|
|
99
|
+
? generateProcessId(processType, computationName || 'general', date)
|
|
100
|
+
: crypto.randomBytes(8).toString('hex');
|
|
101
|
+
|
|
102
|
+
const processLogger = new ProcessLogger(this, processType, processId, computationName, date);
|
|
103
|
+
|
|
104
|
+
this.activeProcesses.set(processId, {
|
|
105
|
+
type: processType,
|
|
106
|
+
computationName,
|
|
107
|
+
date,
|
|
108
|
+
startTime: Date.now(),
|
|
109
|
+
logger: processLogger
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return processLogger;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Ends a tracked process
|
|
117
|
+
*/
|
|
118
|
+
endProcess(processId) {
|
|
119
|
+
const process = this.activeProcesses.get(processId);
|
|
120
|
+
if (process) {
|
|
121
|
+
const duration = Date.now() - process.startTime;
|
|
122
|
+
this.activeProcesses.delete(processId);
|
|
123
|
+
return duration;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The Main Date Analysis Logger (NEW)
|
|
130
|
+
* Aggregates the status of all calculations for a specific date into one readable report.
|
|
131
|
+
*/
|
|
132
|
+
logDateAnalysis(dateStr, analysisReport) {
|
|
133
|
+
const { runnable, blocked, reRuns, resolving } = analysisReport;
|
|
134
|
+
|
|
135
|
+
// 1. Structured Output (Machine Readable)
|
|
136
|
+
if (this.config.enableStructured) {
|
|
137
|
+
console.log(JSON.stringify({
|
|
138
|
+
timestamp: new Date().toISOString(),
|
|
139
|
+
level: 'INFO',
|
|
140
|
+
processType: PROCESS_TYPES.ANALYSIS,
|
|
141
|
+
date: dateStr,
|
|
142
|
+
message: `Date Analysis for ${dateStr}`,
|
|
143
|
+
stats: {
|
|
144
|
+
runnable: runnable.length,
|
|
145
|
+
blocked: blocked.length,
|
|
146
|
+
reRuns: reRuns.length,
|
|
147
|
+
resolving: resolving.length
|
|
148
|
+
},
|
|
149
|
+
details: analysisReport
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 2. Human Readable Output (Console)
|
|
154
|
+
if (this.config.enableConsole) {
|
|
155
|
+
const symbols = { info: 'ℹ️', warn: '⚠️', check: '✅', block: '⛔', link: '🔗', cycle: '🔄' };
|
|
156
|
+
|
|
157
|
+
console.log(`\n🔍 === DATE ANALYSIS REPORT: ${dateStr} ===`);
|
|
158
|
+
|
|
159
|
+
if (reRuns.length) {
|
|
160
|
+
console.log(`\n${symbols.cycle} [HASH MISMATCH / RE-RUNS]`);
|
|
161
|
+
reRuns.forEach(item => {
|
|
162
|
+
console.log(` • ${item.name}: Hash changed (Old: ${item.oldHash?.substring(0,6)}... New: ${item.newHash?.substring(0,6)}...). Cascade: ${item.cascade?.length || 0} dependents.`);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (resolving.length) {
|
|
167
|
+
console.log(`\n${symbols.link} [DEPENDENCY RESOLUTION] (Will run after deps)`);
|
|
168
|
+
resolving.forEach(item => {
|
|
169
|
+
console.log(` • ${item.name}: Waiting for [${item.missingDeps.join(', ')}]`);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (runnable.length) {
|
|
174
|
+
console.log(`\n${symbols.check} [READY TO RUN]`);
|
|
175
|
+
runnable.forEach(item => console.log(` • ${item.name}`));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (blocked.length) {
|
|
179
|
+
console.log(`\n${symbols.block} [BLOCKED / SKIPPED]`);
|
|
180
|
+
blocked.forEach(item => {
|
|
181
|
+
console.log(` • ${item.name}: ${item.reason}`);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
console.log(`\n=============================================\n`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Storage Observability Logger (NEW)
|
|
190
|
+
*/
|
|
191
|
+
logStorage(processId, calcName, date, path, sizeBytes, isSharded) {
|
|
192
|
+
// Standard Log Call with extended metadata
|
|
193
|
+
this.log(LOG_LEVELS.INFO, `Results stored for ${calcName}`, {
|
|
194
|
+
storage: {
|
|
195
|
+
path,
|
|
196
|
+
sizeBytes,
|
|
197
|
+
isSharded,
|
|
198
|
+
sizeMB: (sizeBytes / 1024 / 1024).toFixed(2)
|
|
199
|
+
}
|
|
200
|
+
}, PROCESS_TYPES.STORAGE, processId, calcName, date);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Core logging method
|
|
205
|
+
*/
|
|
206
|
+
log(level, message, context = {}, processType = null, processId = null, computationName = null, date = null) {
|
|
207
|
+
const numericLevel = typeof level === 'string' ? LOG_LEVELS[level] : level;
|
|
208
|
+
|
|
209
|
+
if (numericLevel < this.config.minLevel) return;
|
|
210
|
+
|
|
211
|
+
// Support passing "meta" object in place of context for newer calls
|
|
212
|
+
// Logic: If context contains 'processId' or 'storage', it's likely a meta object
|
|
213
|
+
let finalContext = context;
|
|
214
|
+
let finalMetadata = {};
|
|
215
|
+
let finalStats = undefined;
|
|
216
|
+
let finalStorage = undefined;
|
|
217
|
+
|
|
218
|
+
if (context && (context.processId || context.storage || context.stats)) {
|
|
219
|
+
if (context.processId) processId = context.processId;
|
|
220
|
+
if (context.processType) processType = context.processType;
|
|
221
|
+
if (context.computationName) computationName = context.computationName;
|
|
222
|
+
if (context.date) date = context.date;
|
|
223
|
+
if (context.stats) finalStats = context.stats;
|
|
224
|
+
if (context.storage) finalStorage = context.storage;
|
|
225
|
+
// Clean up context to be just the data remaining
|
|
226
|
+
finalContext = { ...context };
|
|
227
|
+
delete finalContext.processId; delete finalContext.processType;
|
|
228
|
+
delete finalContext.computationName; delete finalContext.date;
|
|
229
|
+
delete finalContext.stats; delete finalContext.storage;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const entry = {
|
|
233
|
+
timestamp: new Date().toISOString(),
|
|
234
|
+
level: Object.keys(LOG_LEVELS).find(k => LOG_LEVELS[k] === numericLevel) || 'INFO',
|
|
235
|
+
processType,
|
|
236
|
+
processId,
|
|
237
|
+
computationName,
|
|
238
|
+
date,
|
|
239
|
+
message,
|
|
240
|
+
context: typeof finalContext === 'string' ? { error: finalContext } : finalContext,
|
|
241
|
+
metadata: finalMetadata,
|
|
242
|
+
stats: finalStats,
|
|
243
|
+
storage: finalStorage
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Add stack trace for errors
|
|
247
|
+
if (numericLevel >= LOG_LEVELS.ERROR && this.config.includeStackTrace && finalContext.stack) {
|
|
248
|
+
entry.metadata.stackTrace = finalContext.stack;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Console output (pretty-printed for development)
|
|
252
|
+
if (this.config.enableConsole) {
|
|
253
|
+
this._consoleLog(entry);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Structured output (for log aggregation systems)
|
|
257
|
+
if (this.config.enableStructured) {
|
|
258
|
+
console.log(formatLogEntry(entry));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Pretty console output for development
|
|
264
|
+
*/
|
|
265
|
+
_consoleLog(entry) {
|
|
266
|
+
const symbols = { TRACE: '🔍', DEBUG: '🐛', INFO: 'ℹ️', WARN: '⚠️', ERROR: '❌', FATAL: '💀' };
|
|
267
|
+
const colors = {
|
|
268
|
+
TRACE: '\x1b[90m', DEBUG: '\x1b[36m', INFO: '\x1b[32m',
|
|
269
|
+
WARN: '\x1b[33m', ERROR: '\x1b[31m', FATAL: '\x1b[35m'
|
|
270
|
+
};
|
|
271
|
+
const reset = '\x1b[0m';
|
|
272
|
+
const color = colors[entry.level] || '';
|
|
273
|
+
const symbol = symbols[entry.level] || 'ℹ️';
|
|
274
|
+
|
|
275
|
+
let output = `${color}${symbol} [${entry.level}]${reset}`;
|
|
276
|
+
|
|
277
|
+
if (entry.processType) output += ` [${entry.processType}]`;
|
|
278
|
+
if (entry.processId) output += ` [${entry.processId.substring(0, 8)}]`;
|
|
279
|
+
if (entry.computationName) output += ` [${entry.computationName}]`;
|
|
280
|
+
if (entry.date) output += ` [${entry.date}]`;
|
|
281
|
+
|
|
282
|
+
output += ` ${entry.message}`;
|
|
283
|
+
|
|
284
|
+
console.log(output);
|
|
285
|
+
|
|
286
|
+
// Print context if present and not empty
|
|
287
|
+
if (entry.context && Object.keys(entry.context).length > 0) {
|
|
288
|
+
console.log(` ${color}Context:${reset}`, entry.context);
|
|
289
|
+
}
|
|
290
|
+
if (entry.storage) {
|
|
291
|
+
console.log(` ${color}Storage:${reset}`, entry.storage);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Convenience methods
|
|
296
|
+
trace(message, context = {}) { this.log(LOG_LEVELS.TRACE, message, context); }
|
|
297
|
+
debug(message, context = {}) { this.log(LOG_LEVELS.DEBUG, message, context); }
|
|
298
|
+
info(message, context = {}) { this.log(LOG_LEVELS.INFO, message, context); }
|
|
299
|
+
warn(message, context = {}) { this.log(LOG_LEVELS.WARN, message, context); }
|
|
300
|
+
error(message, context = {}) { this.log(LOG_LEVELS.ERROR, message, context); }
|
|
301
|
+
fatal(message, context = {}) { this.log(LOG_LEVELS.FATAL, message, context); }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Process-scoped Logger
|
|
306
|
+
* Automatically includes process context in all log calls
|
|
307
|
+
*/
|
|
308
|
+
class ProcessLogger {
|
|
309
|
+
constructor(parent, processType, processId, computationName, date) {
|
|
310
|
+
this.parent = parent;
|
|
311
|
+
this.processType = processType;
|
|
312
|
+
this.processId = processId;
|
|
313
|
+
this.computationName = computationName;
|
|
314
|
+
this.date = date;
|
|
315
|
+
this.startTime = Date.now();
|
|
316
|
+
this.metrics = {
|
|
317
|
+
operations: 0,
|
|
318
|
+
errors: 0,
|
|
319
|
+
warnings: 0
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
log(level, message, context = {}) {
|
|
324
|
+
const numericLevel = typeof level === 'string' ? LOG_LEVELS[level] : level;
|
|
325
|
+
|
|
326
|
+
this.metrics.operations++;
|
|
327
|
+
if (numericLevel === LOG_LEVELS.ERROR || numericLevel === LOG_LEVELS.FATAL) {
|
|
328
|
+
this.metrics.errors++;
|
|
329
|
+
}
|
|
330
|
+
if (numericLevel === LOG_LEVELS.WARN) {
|
|
331
|
+
this.metrics.warnings++;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
this.parent.log(
|
|
335
|
+
level,
|
|
336
|
+
message,
|
|
337
|
+
context,
|
|
338
|
+
this.processType,
|
|
339
|
+
this.processId,
|
|
340
|
+
this.computationName,
|
|
341
|
+
this.date
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Complete the process and log summary
|
|
347
|
+
*/
|
|
348
|
+
complete(success = true, finalMessage = null) {
|
|
349
|
+
const duration = Date.now() - this.startTime;
|
|
350
|
+
const level = success ? LOG_LEVELS.INFO : LOG_LEVELS.ERROR;
|
|
351
|
+
|
|
352
|
+
const summaryMessage = finalMessage || (success
|
|
353
|
+
? `Process completed successfully`
|
|
354
|
+
: `Process completed with errors`);
|
|
355
|
+
|
|
356
|
+
this.log(level, summaryMessage, {
|
|
357
|
+
stats: {
|
|
358
|
+
duration: `${duration}ms`,
|
|
359
|
+
durationMs: duration,
|
|
360
|
+
operations: this.metrics.operations,
|
|
361
|
+
errors: this.metrics.errors,
|
|
362
|
+
warnings: this.metrics.warnings,
|
|
363
|
+
success
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
this.parent.endProcess(this.processId);
|
|
368
|
+
return duration;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Convenience methods
|
|
372
|
+
trace(message, context = {}) { this.log(LOG_LEVELS.TRACE, message, context); }
|
|
373
|
+
debug(message, context = {}) { this.log(LOG_LEVELS.DEBUG, message, context); }
|
|
374
|
+
info(message, context = {}) { this.log(LOG_LEVELS.INFO, message, context); }
|
|
375
|
+
warn(message, context = {}) { this.log(LOG_LEVELS.WARN, message, context); }
|
|
376
|
+
error(message, context = {}) { this.log(LOG_LEVELS.ERROR, message, context); }
|
|
377
|
+
fatal(message, context = {}) { this.log(LOG_LEVELS.FATAL, message, context); }
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Log Message Templates
|
|
382
|
+
*/
|
|
383
|
+
const LOG_TEMPLATES = {
|
|
384
|
+
// Schema Generation
|
|
385
|
+
SCHEMA_SUCCESS: (computationName) =>
|
|
386
|
+
`Schema generation successful for ${computationName}`,
|
|
387
|
+
SCHEMA_FAILURE: (computationName, reason) =>
|
|
388
|
+
`Schema generation failed for ${computationName}: ${reason}`,
|
|
389
|
+
|
|
390
|
+
// Computation Execution
|
|
391
|
+
COMPUTATION_START: (computationName, date) =>
|
|
392
|
+
`Starting computation ${computationName} for ${date}`,
|
|
393
|
+
COMPUTATION_SUCCESS: (computationName, date) =>
|
|
394
|
+
`Computation successful for ${computationName} on ${date}`,
|
|
395
|
+
COMPUTATION_FAILURE: (computationName, date, reason) =>
|
|
396
|
+
`Computation failed for ${computationName} on ${date}: ${reason}`,
|
|
397
|
+
|
|
398
|
+
// Storage
|
|
399
|
+
STORAGE_SUCCESS: (computationName, date, path, size) =>
|
|
400
|
+
`Results stored for ${computationName} on ${date} at ${path} (${size} bytes)`,
|
|
401
|
+
STORAGE_FAILURE: (computationName, date, path, reason) =>
|
|
402
|
+
`Failed to store results for ${computationName} on ${date} at ${path}: ${reason}`,
|
|
403
|
+
|
|
404
|
+
// Hash Validation
|
|
405
|
+
HASH_MISMATCH: (computationName, storedHash, currentHash) =>
|
|
406
|
+
`Hash mismatch for ${computationName}: stored=${storedHash}, current=${currentHash}`,
|
|
407
|
+
HASH_MATCH: (computationName) =>
|
|
408
|
+
`Hash match for ${computationName}, no code changes detected`,
|
|
409
|
+
HASH_CASCADE: (computationName, affectedComputations) =>
|
|
410
|
+
`Code change in ${computationName} will cascade to: ${affectedComputations.join(', ')}`,
|
|
411
|
+
|
|
412
|
+
// Manifest
|
|
413
|
+
MANIFEST_SUCCESS: (computationCount) =>
|
|
414
|
+
`Manifest built successfully with ${computationCount} computations`,
|
|
415
|
+
MANIFEST_TREE: (tree) =>
|
|
416
|
+
`Dependency tree:\n${tree}`,
|
|
417
|
+
|
|
418
|
+
// Date Analysis
|
|
419
|
+
DATE_ANALYSIS: (date, runnable, notRunnable) =>
|
|
420
|
+
`Date ${date}: ${runnable.length} runnable, ${notRunnable.length} blocked`,
|
|
421
|
+
DATE_MISSING_ROOTDATA: (date, computationName, missingData) =>
|
|
422
|
+
`${computationName} on ${date}: Cannot run due to missing root data: ${missingData.join(', ')}`,
|
|
423
|
+
DATE_MISSING_DEPENDENCY: (date, computationName, missingDep) =>
|
|
424
|
+
`${computationName} on ${date}: Will resolve after ${missingDep} completes`,
|
|
425
|
+
DATE_HASH_RERUN: (date, computationName, affectedDeps) =>
|
|
426
|
+
`${computationName} on ${date}: Hash mismatch, re-running (affects: ${affectedDeps.join(', ')})`,
|
|
427
|
+
|
|
428
|
+
// Availability
|
|
429
|
+
DATA_AVAILABLE: (date, types) =>
|
|
430
|
+
`Data available for ${date}: ${types.join(', ')}`,
|
|
431
|
+
DATA_MISSING: (date, types) =>
|
|
432
|
+
`Data missing for ${date}: ${types.join(', ')}`,
|
|
433
|
+
|
|
434
|
+
// Dispatch/Worker
|
|
435
|
+
DISPATCH_START: (pass, dateCount) =>
|
|
436
|
+
`Dispatching Pass ${pass} for ${dateCount} dates`,
|
|
437
|
+
DISPATCH_COMPLETE: (pass, dispatched) =>
|
|
438
|
+
`Dispatch complete: ${dispatched} tasks for Pass ${pass}`,
|
|
439
|
+
WORKER_TASK_START: (date, pass) =>
|
|
440
|
+
`Worker starting task: Date=${date}, Pass=${pass}`,
|
|
441
|
+
WORKER_TASK_COMPLETE: (date, pass, updateCount) =>
|
|
442
|
+
`Worker completed task: Date=${date}, Pass=${pass}, Updates=${updateCount}`,
|
|
443
|
+
WORKER_TASK_SKIP: (date, pass, reason) =>
|
|
444
|
+
`Worker skipped task: Date=${date}, Pass=${pass}, Reason=${reason}`
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
module.exports = {
|
|
448
|
+
StructuredLogger,
|
|
449
|
+
ProcessLogger,
|
|
450
|
+
LOG_LEVELS,
|
|
451
|
+
PROCESS_TYPES,
|
|
452
|
+
LOG_TEMPLATES,
|
|
453
|
+
generateProcessId
|
|
454
|
+
};
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Handles saving computation results with
|
|
2
|
+
* @fileoverview Handles saving computation results with observability.
|
|
3
3
|
*/
|
|
4
4
|
const { commitBatchInChunks } = require('./FirestoreUtils');
|
|
5
5
|
const { updateComputationStatus } = require('./StatusRepository');
|
|
6
6
|
const { batchStoreSchemas } = require('../utils/schema_capture');
|
|
7
|
+
const { generateProcessId, PROCESS_TYPES } = require('../logger/logger');
|
|
7
8
|
|
|
8
9
|
async function commitResults(stateObj, dStr, passName, config, deps, skipStatusWrite = false) {
|
|
9
10
|
const successUpdates = {};
|
|
10
11
|
const schemas = [];
|
|
12
|
+
const { logger } = deps;
|
|
13
|
+
const pid = generateProcessId(PROCESS_TYPES.STORAGE, passName, dStr);
|
|
11
14
|
|
|
12
15
|
for (const name in stateObj) {
|
|
13
16
|
const calc = stateObj[name];
|
|
14
17
|
try {
|
|
15
18
|
const result = await calc.getResult();
|
|
16
|
-
if (!result)
|
|
17
|
-
deps.logger.log('INFO', `${name} for ${dStr}: Skipped (Empty Result)`);
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
19
|
+
if (!result) continue;
|
|
20
20
|
|
|
21
21
|
const mainDocRef = deps.db.collection(config.resultsCollection)
|
|
22
22
|
.doc(dStr)
|
|
@@ -25,8 +25,9 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
25
25
|
.collection(config.computationsSubcollection)
|
|
26
26
|
.doc(name);
|
|
27
27
|
|
|
28
|
-
const updates = await prepareAutoShardedWrites(result, mainDocRef,
|
|
28
|
+
const updates = await prepareAutoShardedWrites(result, mainDocRef, logger);
|
|
29
29
|
|
|
30
|
+
// Capture Schema
|
|
30
31
|
if (calc.manifest.class.getSchema) {
|
|
31
32
|
const { class: _cls, ...safeMetadata } = calc.manifest;
|
|
32
33
|
schemas.push({
|
|
@@ -38,15 +39,18 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
if (updates.length > 0) {
|
|
42
|
+
const totalSize = updates.reduce((acc, u) => acc + (u.data ? JSON.stringify(u.data).length : 0), 0);
|
|
43
|
+
const isSharded = updates.some(u => u.data._sharded === true);
|
|
44
|
+
|
|
41
45
|
await commitBatchInChunks(config, deps, updates, `${name} Results`);
|
|
46
|
+
|
|
47
|
+
// Structured Storage Log
|
|
48
|
+
logger.logStorage(pid, name, dStr, mainDocRef.path, totalSize, isSharded);
|
|
49
|
+
|
|
42
50
|
successUpdates[name] = calc.manifest.hash || true;
|
|
43
|
-
const isSharded = updates.some(u => u.data._sharded === true);
|
|
44
|
-
deps.logger.log('INFO', `${name} for ${dStr}: \u2714 Success (Written ${isSharded ? 'Sharded' : 'Standard'})`);
|
|
45
|
-
} else {
|
|
46
|
-
deps.logger.log('INFO', `${name} for ${dStr}: - Empty Data`);
|
|
47
51
|
}
|
|
48
52
|
} catch (e) {
|
|
49
|
-
|
|
53
|
+
logger.log('ERROR', `Commit failed for ${name}`, { processId: pid, error: e.message });
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
|
|
@@ -54,12 +58,14 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
54
58
|
|
|
55
59
|
if (!skipStatusWrite && Object.keys(successUpdates).length > 0) {
|
|
56
60
|
await updateComputationStatus(dStr, successUpdates, config, deps);
|
|
57
|
-
deps.logger.log('INFO', `[${passName}] Updated status document for ${Object.keys(successUpdates).length} successful computations.`);
|
|
58
61
|
}
|
|
59
62
|
return successUpdates;
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
// ... rest of file (calculateFirestoreBytes, prepareAutoShardedWrites) remains same ...
|
|
66
|
+
// Just ensure prepareAutoShardedWrites uses the provided logger if it logs internal warnings.
|
|
62
67
|
function calculateFirestoreBytes(value) {
|
|
68
|
+
// ... same as before
|
|
63
69
|
if (value === null) return 1;
|
|
64
70
|
if (value === undefined) return 0;
|
|
65
71
|
if (typeof value === 'boolean') return 1;
|
|
@@ -73,7 +79,9 @@ function calculateFirestoreBytes(value) {
|
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
async function prepareAutoShardedWrites(result, docRef, logger) {
|
|
76
|
-
|
|
82
|
+
// ... same logic, just ensure existing logs inside here use the logger properly if needed
|
|
83
|
+
// Copied from previous logic, essentially checks size > 900KB and splits
|
|
84
|
+
const SAFETY_THRESHOLD_BYTES = 1000 * 1024;
|
|
77
85
|
const OVERHEAD_ALLOWANCE = 20 * 1024;
|
|
78
86
|
const CHUNK_LIMIT = SAFETY_THRESHOLD_BYTES - OVERHEAD_ALLOWANCE;
|
|
79
87
|
const totalSize = calculateFirestoreBytes(result);
|
|
@@ -84,19 +92,16 @@ async function prepareAutoShardedWrites(result, docRef, logger) {
|
|
|
84
92
|
let currentChunkSize = 0;
|
|
85
93
|
let shardIndex = 0;
|
|
86
94
|
|
|
87
|
-
// If under limit, write directly
|
|
88
95
|
if ((totalSize + docPathSize) < CHUNK_LIMIT) { const data = { ...result, _completed: true, _sharded: false }; return [{ ref: docRef, data, options: { merge: true } }]; }
|
|
89
96
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// iF over limit, shard the document
|
|
97
|
+
// Note: We don't log "Sharding..." here anymore because we log the structured event in commitResults
|
|
98
|
+
|
|
93
99
|
for (const [key, value] of Object.entries(result)) {
|
|
94
100
|
if (key.startsWith('_')) continue;
|
|
95
101
|
const keySize = Buffer.byteLength(key, 'utf8') + 1;
|
|
96
102
|
const valueSize = calculateFirestoreBytes(value);
|
|
97
103
|
const itemSize = keySize + valueSize;
|
|
98
104
|
|
|
99
|
-
// If adding this item exceeds the chunk limit, commit current chunk
|
|
100
105
|
if (currentChunkSize + itemSize > CHUNK_LIMIT) {
|
|
101
106
|
writes.push({ ref: shardCollection.doc(`shard_${shardIndex}`), data: currentChunk, options: { merge: false } });
|
|
102
107
|
shardIndex++;
|
|
@@ -107,10 +112,8 @@ async function prepareAutoShardedWrites(result, docRef, logger) {
|
|
|
107
112
|
currentChunkSize += itemSize;
|
|
108
113
|
}
|
|
109
114
|
|
|
110
|
-
// Write the final chunk
|
|
111
115
|
if (Object.keys(currentChunk).length > 0) { writes.push({ ref: shardCollection.doc(`shard_${shardIndex}`), data: currentChunk, options: { merge: false } }); }
|
|
112
116
|
|
|
113
|
-
// Finally, write the pointer document
|
|
114
117
|
const pointerData = { _completed: true, _sharded: true, _shardCount: shardIndex + 1, _lastUpdated: new Date().toISOString() };
|
|
115
118
|
writes.push({ ref: docRef, data: pointerData, options: { merge: false } });
|
|
116
119
|
return writes;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Singleton Loader for the Manifest.
|
|
3
|
+
* Prevents expensive manifest rebuilding on every function invocation if not needed.
|
|
4
|
+
*/
|
|
5
|
+
const { build } = require('../context/ManifestBuilder');
|
|
6
|
+
const { StructuredLogger, PROCESS_TYPES, generateProcessId } = require('../logger/logger');
|
|
7
|
+
|
|
8
|
+
// Cache the manifest in global scope (warm start optimization)
|
|
9
|
+
let cachedManifest = null;
|
|
10
|
+
|
|
11
|
+
function getManifest(productLines = [], calculationsDir, dependencies = {}) {
|
|
12
|
+
if (cachedManifest) {
|
|
13
|
+
return cachedManifest;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const logger = dependencies.logger || new StructuredLogger();
|
|
17
|
+
const pid = generateProcessId(PROCESS_TYPES.MANIFEST, 'build', new Date().toISOString().slice(0,10));
|
|
18
|
+
|
|
19
|
+
logger.log('INFO', 'Starting Manifest Build...', { processId: pid });
|
|
20
|
+
|
|
21
|
+
const startTime = Date.now();
|
|
22
|
+
try {
|
|
23
|
+
cachedManifest = build(productLines, calculationsDir);
|
|
24
|
+
|
|
25
|
+
// Log Topology Stats
|
|
26
|
+
const passCounts = {};
|
|
27
|
+
cachedManifest.forEach(c => { passCounts[c.pass] = (passCounts[c.pass] || 0) + 1; });
|
|
28
|
+
|
|
29
|
+
logger.log('INFO', 'Manifest Build Success', {
|
|
30
|
+
processId: pid,
|
|
31
|
+
durationMs: Date.now() - startTime,
|
|
32
|
+
totalCalculations: cachedManifest.length,
|
|
33
|
+
topology: passCounts
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return cachedManifest;
|
|
37
|
+
} catch (e) {
|
|
38
|
+
logger.log('FATAL', 'Manifest Build Failed', { processId: pid, error: e.message });
|
|
39
|
+
throw e;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { getManifest };
|