bulltrackers-module 1.0.223 → 1.0.224
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 +126 -80
- 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,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Main Orchestrator. Coordinates the topological execution
|
|
2
|
+
* @fileoverview Main Orchestrator. Coordinates the topological execution.
|
|
3
|
+
* UPDATED: Includes comprehensive Date Analysis logic.
|
|
3
4
|
*/
|
|
4
5
|
const { normalizeName, getExpectedDateStrings } = require('./utils/utils');
|
|
5
|
-
const { checkRootDataAvailability
|
|
6
|
+
const { checkRootDataAvailability } = require('./data/AvailabilityChecker');
|
|
6
7
|
const { fetchExistingResults } = require('./data/DependencyFetcher');
|
|
7
8
|
const { fetchComputationStatus, updateComputationStatus } = require('./persistence/StatusRepository');
|
|
8
9
|
const { runBatchPriceComputation } = require('./executors/PriceBatchExecutor');
|
|
9
10
|
const { StandardExecutor } = require('./executors/StandardExecutor');
|
|
10
11
|
const { MetaExecutor } = require('./executors/MetaExecutor');
|
|
12
|
+
const { generateProcessId, PROCESS_TYPES } = require('./logger/logger');
|
|
11
13
|
|
|
12
14
|
const PARALLEL_BATCH_SIZE = 7;
|
|
13
15
|
|
|
@@ -18,135 +20,179 @@ function groupByPass(manifest) {
|
|
|
18
20
|
}, {});
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Performs the logical analysis requested by the user.
|
|
25
|
+
* Determines exactly what can run, what is blocked, and why.
|
|
26
|
+
*/
|
|
27
|
+
function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus, manifestMap) {
|
|
28
|
+
const report = {
|
|
29
|
+
runnable: [],
|
|
30
|
+
blocked: [], // Missing Root Data
|
|
31
|
+
resolving: [], // Missing Dependency (but might be fixed by run)
|
|
32
|
+
reRuns: [] // Hash Mismatch
|
|
33
|
+
};
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
price: new Date('2025-08-01T00:00:00Z')
|
|
35
|
+
// Helper: Is a dependency satisfied?
|
|
36
|
+
const isDepSatisfied = (depName, dailyStatus) => {
|
|
37
|
+
const norm = normalizeName(depName);
|
|
38
|
+
if (dailyStatus[norm]) return true; // It exists
|
|
39
|
+
return false;
|
|
34
40
|
};
|
|
35
|
-
earliestDates.absoluteEarliest = Object.values(earliestDates).reduce((a, b) => a < b ? a : b);
|
|
36
|
-
const passes = groupByPass(computationManifest);
|
|
37
|
-
const calcsInThisPass = passes[passToRun] || [];
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
for (const calc of calcsInPass) {
|
|
43
|
+
const cName = normalizeName(calc.name);
|
|
44
|
+
const storedHash = dailyStatus[cName];
|
|
45
|
+
const currentHash = calc.hash;
|
|
40
46
|
|
|
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);
|
|
47
|
+
// 1. Root Data Check
|
|
48
|
+
const missingRoots = [];
|
|
49
|
+
if (calc.rootDataDependencies) {
|
|
50
|
+
for (const dep of calc.rootDataDependencies) {
|
|
51
|
+
if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missingRoots.push('portfolio');
|
|
52
|
+
if (dep === 'insights' && !rootDataStatus.hasInsights) missingRoots.push('insights');
|
|
53
|
+
if (dep === 'social' && !rootDataStatus.hasSocial) missingRoots.push('social');
|
|
54
|
+
if (dep === 'history' && !rootDataStatus.hasHistory) missingRoots.push('history');
|
|
55
|
+
if (dep === 'price' && !rootDataStatus.hasPrices) missingRoots.push('price');
|
|
56
|
+
}
|
|
54
57
|
}
|
|
55
|
-
}
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
if (missingRoots.length > 0) {
|
|
60
|
+
report.blocked.push({ name: cName, reason: `Missing Root Data: ${missingRoots.join(', ')}` });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 2. Hash / Version Check
|
|
65
|
+
let isReRun = false;
|
|
66
|
+
if (storedHash && storedHash !== currentHash) {
|
|
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
|
|
84
|
+
const missingDeps = [];
|
|
85
|
+
if (calc.dependencies) {
|
|
86
|
+
for (const dep of calc.dependencies) {
|
|
87
|
+
// If it's a historical dependency (yesterday's data), we assume availability or check elsewhere
|
|
88
|
+
// If it's a current pass dependency:
|
|
89
|
+
if (!isDepSatisfied(dep, dailyStatus)) {
|
|
90
|
+
missingDeps.push(dep);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
58
94
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
95
|
+
if (missingDeps.length > 0) {
|
|
96
|
+
// It's missing a dependency. Is it fatal?
|
|
97
|
+
// Since we are inside a Pass, typically dependencies are from PREVIOUS passes.
|
|
98
|
+
// If it's from a previous pass and missing, it's blocked.
|
|
99
|
+
// But for the sake of the report:
|
|
100
|
+
report.resolving.push({ name: cName, missingDeps });
|
|
101
|
+
// In strict execution, this might be "runnable" if we assume the dep runs first in this batch,
|
|
102
|
+
// but usually dependencies are strictly lower passes.
|
|
103
|
+
// We'll mark it as resolving/blocked.
|
|
104
|
+
} else {
|
|
105
|
+
// Dependencies met.
|
|
106
|
+
if (!dailyStatus[cName] || isReRun) {
|
|
107
|
+
report.runnable.push(calc);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
64
110
|
}
|
|
111
|
+
|
|
112
|
+
return report;
|
|
65
113
|
}
|
|
66
114
|
|
|
67
115
|
async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, dependencies, computationManifest) {
|
|
68
116
|
const { logger } = dependencies;
|
|
117
|
+
const orchestratorPid = generateProcessId(PROCESS_TYPES.ORCHESTRATOR, passToRun, dateStr);
|
|
118
|
+
|
|
69
119
|
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
70
120
|
|
|
71
|
-
// 1.
|
|
72
|
-
const dailyStatus
|
|
73
|
-
const calcsToAttempt = [];
|
|
121
|
+
// 1. Fetch State
|
|
122
|
+
const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
|
|
74
123
|
|
|
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
|
|
124
|
+
// 2. Check Data Availability (One shot)
|
|
96
125
|
const earliestDates = {
|
|
97
126
|
portfolio: new Date('2025-09-25T00:00:00Z'),
|
|
98
|
-
history: new Date('2025-11-05T00:00:00Z'),
|
|
127
|
+
history: new Date('2025-11-05T00:00:00Z'), // Configurable in prod
|
|
99
128
|
social: new Date('2025-10-30T00:00:00Z'),
|
|
100
129
|
insights: new Date('2025-08-26T00:00:00Z'),
|
|
101
130
|
price: new Date('2025-08-01T00:00:00Z')
|
|
102
131
|
};
|
|
103
|
-
|
|
132
|
+
|
|
104
133
|
const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
|
|
105
|
-
|
|
134
|
+
const rootStatus = rootData ? rootData.status : { hasPortfolio: false, hasPrices: false, hasInsights: false, hasSocial: false, hasHistory: false };
|
|
135
|
+
|
|
136
|
+
// 3. PERFORM DATE ANALYSIS (Log the report)
|
|
137
|
+
const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
|
|
138
|
+
const analysisReport = analyzeDateExecution(dateStr, calcsInThisPass, rootStatus, dailyStatus, manifestMap);
|
|
106
139
|
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
140
|
+
// LOG THE REPORT
|
|
141
|
+
logger.logDateAnalysis(dateStr, analysisReport);
|
|
142
|
+
|
|
143
|
+
// 4. Filter Run List based on Analysis
|
|
144
|
+
// We combine 'runnable' and 'reRuns'. 'resolving' are skipped (unless we support intra-pass resolution).
|
|
145
|
+
// Note: analyzeDateExecution returns plain objects or calc objects. We need the calc objects.
|
|
146
|
+
const calcsToRunNames = new Set([
|
|
147
|
+
...analysisReport.runnable.map(c => c.name),
|
|
148
|
+
...analysisReport.reRuns.map(c => c.name) // Re-runs are valid to run
|
|
149
|
+
]);
|
|
111
150
|
|
|
112
|
-
|
|
113
|
-
|
|
151
|
+
const finalRunList = calcsInThisPass.filter(c => calcsToRunNames.has(normalizeName(c.name)));
|
|
152
|
+
|
|
153
|
+
if (!finalRunList.length) {
|
|
114
154
|
return null;
|
|
115
155
|
}
|
|
116
156
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
157
|
+
logger.log('INFO', `[Orchestrator] Executing ${finalRunList.length} calculations for ${dateStr}`, { processId: orchestratorPid });
|
|
158
|
+
|
|
159
|
+
// 5. Execution
|
|
160
|
+
const standardToRun = finalRunList.filter(c => c.type === 'standard');
|
|
161
|
+
const metaToRun = finalRunList.filter(c => c.type === 'meta');
|
|
120
162
|
|
|
121
163
|
const dateUpdates = {};
|
|
122
164
|
|
|
123
165
|
try {
|
|
124
166
|
const calcsRunning = [...standardToRun, ...metaToRun];
|
|
167
|
+
|
|
168
|
+
// Fetch dependencies (Previous pass results)
|
|
125
169
|
const existingResults = await fetchExistingResults(dateStr, calcsRunning, computationManifest, config, dependencies, false);
|
|
170
|
+
|
|
171
|
+
// Fetch Yesterday's results (if Historical)
|
|
126
172
|
const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
127
173
|
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
128
174
|
const previousResults = await fetchExistingResults(prevDateStr, calcsRunning, computationManifest, config, dependencies, true);
|
|
129
175
|
|
|
130
176
|
if (standardToRun.length) {
|
|
131
|
-
const updates = await StandardExecutor.run(dateToProcess, standardToRun, `Pass ${passToRun}
|
|
177
|
+
const updates = await StandardExecutor.run(dateToProcess, standardToRun, `Pass ${passToRun}`, config, dependencies, rootData, existingResults, previousResults, false);
|
|
132
178
|
Object.assign(dateUpdates, updates);
|
|
133
179
|
}
|
|
134
180
|
if (metaToRun.length) {
|
|
135
|
-
const updates = await MetaExecutor.run(dateToProcess, metaToRun, `Pass ${passToRun}
|
|
181
|
+
const updates = await MetaExecutor.run(dateToProcess, metaToRun, `Pass ${passToRun}`, config, dependencies, existingResults, previousResults, rootData, false);
|
|
136
182
|
Object.assign(dateUpdates, updates);
|
|
137
183
|
}
|
|
138
184
|
|
|
139
185
|
} catch (err) {
|
|
140
|
-
logger.log('ERROR', `[
|
|
141
|
-
[...standardToRun, ...metaToRun].forEach(c => dateUpdates[normalizeName(c.name)] = false);
|
|
186
|
+
logger.log('ERROR', `[Orchestrator] Failed execution for ${dateStr}`, { processId: orchestratorPid, error: err.message });
|
|
142
187
|
throw err;
|
|
143
188
|
}
|
|
144
189
|
|
|
190
|
+
// 6. Status Update happens inside Executors, but we can log final success here
|
|
145
191
|
if (Object.keys(dateUpdates).length > 0) {
|
|
146
|
-
await updateComputationStatus(
|
|
192
|
+
// await updateComputationStatus... (Handled by Executors currently)
|
|
147
193
|
}
|
|
148
194
|
|
|
149
195
|
return { date: dateStr, updates: dateUpdates };
|
|
150
196
|
}
|
|
151
197
|
|
|
152
|
-
module.exports = {
|
|
198
|
+
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 };
|