bulltrackers-module 1.0.733 → 1.0.734
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-v2/README.md +152 -0
- package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +720 -0
- package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +176 -0
- package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +294 -0
- package/functions/computation-system-v2/computations/TestComputation.js +46 -0
- package/functions/computation-system-v2/computations/UserPortfolioSummary.js +172 -0
- package/functions/computation-system-v2/config/bulltrackers.config.js +317 -0
- package/functions/computation-system-v2/framework/core/Computation.js +73 -0
- package/functions/computation-system-v2/framework/core/Manifest.js +223 -0
- package/functions/computation-system-v2/framework/core/RuleInjector.js +53 -0
- package/functions/computation-system-v2/framework/core/Rules.js +231 -0
- package/functions/computation-system-v2/framework/core/RunAnalyzer.js +163 -0
- package/functions/computation-system-v2/framework/cost/CostTracker.js +154 -0
- package/functions/computation-system-v2/framework/data/DataFetcher.js +399 -0
- package/functions/computation-system-v2/framework/data/QueryBuilder.js +232 -0
- package/functions/computation-system-v2/framework/data/SchemaRegistry.js +287 -0
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +498 -0
- package/functions/computation-system-v2/framework/execution/TaskRunner.js +35 -0
- package/functions/computation-system-v2/framework/execution/middleware/CostTrackerMiddleware.js +32 -0
- package/functions/computation-system-v2/framework/execution/middleware/LineageMiddleware.js +32 -0
- package/functions/computation-system-v2/framework/execution/middleware/Middleware.js +14 -0
- package/functions/computation-system-v2/framework/execution/middleware/ProfilerMiddleware.js +47 -0
- package/functions/computation-system-v2/framework/index.js +45 -0
- package/functions/computation-system-v2/framework/lineage/LineageTracker.js +147 -0
- package/functions/computation-system-v2/framework/monitoring/Profiler.js +80 -0
- package/functions/computation-system-v2/framework/resilience/Checkpointer.js +66 -0
- package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +327 -0
- package/functions/computation-system-v2/framework/storage/StateRepository.js +286 -0
- package/functions/computation-system-v2/framework/storage/StorageManager.js +469 -0
- package/functions/computation-system-v2/framework/storage/index.js +9 -0
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +86 -0
- package/functions/computation-system-v2/framework/utils/Graph.js +205 -0
- package/functions/computation-system-v2/handlers/dispatcher.js +109 -0
- package/functions/computation-system-v2/handlers/index.js +23 -0
- package/functions/computation-system-v2/handlers/onDemand.js +289 -0
- package/functions/computation-system-v2/handlers/scheduler.js +327 -0
- package/functions/computation-system-v2/index.js +163 -0
- package/functions/computation-system-v2/rules/index.js +49 -0
- package/functions/computation-system-v2/rules/instruments.js +465 -0
- package/functions/computation-system-v2/rules/metrics.js +304 -0
- package/functions/computation-system-v2/rules/portfolio.js +534 -0
- package/functions/computation-system-v2/rules/rankings.js +655 -0
- package/functions/computation-system-v2/rules/social.js +562 -0
- package/functions/computation-system-v2/rules/trades.js +545 -0
- package/functions/computation-system-v2/scripts/migrate-sectors.js +73 -0
- package/functions/computation-system-v2/test/test-dispatcher.js +317 -0
- package/functions/computation-system-v2/test/test-framework.js +500 -0
- package/functions/computation-system-v2/test/test-real-execution.js +166 -0
- package/functions/computation-system-v2/test/test-real-integration.js +194 -0
- package/functions/computation-system-v2/test/test-refactor-e2e.js +131 -0
- package/functions/computation-system-v2/test/test-results.json +31 -0
- package/functions/computation-system-v2/test/test-risk-metrics-computation.js +329 -0
- package/functions/computation-system-v2/test/test-scheduler.js +204 -0
- package/functions/computation-system-v2/test/test-storage.js +449 -0
- package/functions/orchestrator/index.js +18 -26
- package/package.json +3 -2
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Orchestrator (Refactored Executor)
|
|
3
|
+
* * Responsibilities:
|
|
4
|
+
* 1. Graph & Schedule Management (Manifest, Passes, DAG)
|
|
5
|
+
* 2. Data Provisioning (Fetching Data, Loading Dependencies, Reference Data)
|
|
6
|
+
* 3. Execution Strategy (Streaming vs. In-Memory)
|
|
7
|
+
* 4. Delegation (Hands off actual 'work' to TaskRunner + Middleware)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const pLimit = require('p-limit');
|
|
12
|
+
|
|
13
|
+
// Core Components
|
|
14
|
+
const { ManifestBuilder } = require('../core/Manifest');
|
|
15
|
+
const { RulesRegistry } = require('../core/Rules');
|
|
16
|
+
const { RuleInjector } = require('../core/RuleInjector');
|
|
17
|
+
const { RunAnalyzer } = require('../core/RunAnalyzer');
|
|
18
|
+
|
|
19
|
+
// Data & Storage
|
|
20
|
+
const { SchemaRegistry } = require('../data/SchemaRegistry');
|
|
21
|
+
const { QueryBuilder } = require('../data/QueryBuilder');
|
|
22
|
+
const { DataFetcher } = require('../data/DataFetcher');
|
|
23
|
+
const { StorageManager } = require('../storage/StorageManager');
|
|
24
|
+
const { StateRepository } = require('../storage/StateRepository');
|
|
25
|
+
const { Checkpointer } = require('../resilience/Checkpointer');
|
|
26
|
+
|
|
27
|
+
// Execution Components
|
|
28
|
+
const { TaskRunner } = require('./TaskRunner');
|
|
29
|
+
const { ProfilerMiddleware } = require('./middleware/ProfilerMiddleware');
|
|
30
|
+
const { CostTrackerMiddleware } = require('./middleware/CostTrackerMiddleware');
|
|
31
|
+
const { LineageMiddleware } = require('./middleware/LineageMiddleware');
|
|
32
|
+
|
|
33
|
+
const DEFAULT_CONCURRENCY = 20;
|
|
34
|
+
const BATCH_SIZE = 1000;
|
|
35
|
+
|
|
36
|
+
class Orchestrator {
|
|
37
|
+
constructor(config, logger = null) {
|
|
38
|
+
this.config = config;
|
|
39
|
+
this.logger = logger || console;
|
|
40
|
+
|
|
41
|
+
// 1. Initialize Base Services
|
|
42
|
+
this.schemaRegistry = new SchemaRegistry(config.bigquery, this.logger);
|
|
43
|
+
this.queryBuilder = new QueryBuilder(config.bigquery, this.schemaRegistry, this.logger);
|
|
44
|
+
this.dataFetcher = new DataFetcher({ ...config.bigquery, tables: config.tables }, this.queryBuilder, this.logger);
|
|
45
|
+
this.storageManager = new StorageManager(config, this.logger);
|
|
46
|
+
this.stateRepository = new StateRepository(config, this.logger);
|
|
47
|
+
|
|
48
|
+
// 2. Initialize Logic & Rules
|
|
49
|
+
this.manifestBuilder = new ManifestBuilder(config, this.logger);
|
|
50
|
+
const rulesRegistry = new RulesRegistry(config, this.logger);
|
|
51
|
+
this.ruleInjector = new RuleInjector(rulesRegistry);
|
|
52
|
+
|
|
53
|
+
// 3. Initialize Execution Stack (Middleware)
|
|
54
|
+
const profiler = new ProfilerMiddleware(config);
|
|
55
|
+
profiler.setStorage(this.storageManager);
|
|
56
|
+
|
|
57
|
+
this.lineageMiddleware = new LineageMiddleware(config);
|
|
58
|
+
const costTracker = new CostTrackerMiddleware(config);
|
|
59
|
+
|
|
60
|
+
// The "Onion": Cost( Lineage( Profiler( Task ) ) )
|
|
61
|
+
this.runner = new TaskRunner([
|
|
62
|
+
costTracker,
|
|
63
|
+
this.lineageMiddleware,
|
|
64
|
+
profiler
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
// State
|
|
68
|
+
this.manifest = null;
|
|
69
|
+
this.runAnalyzer = null;
|
|
70
|
+
this.referenceDataCache = {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async initialize() {
|
|
74
|
+
this._log('INFO', 'Initializing Orchestrator...');
|
|
75
|
+
|
|
76
|
+
// Build Manifest
|
|
77
|
+
this.manifest = this.manifestBuilder.build(this.config.computations || []);
|
|
78
|
+
|
|
79
|
+
// Initialize Analyzer
|
|
80
|
+
this.runAnalyzer = new RunAnalyzer(this.manifest, this.dataFetcher, this.logger);
|
|
81
|
+
|
|
82
|
+
// Warm Schema Cache
|
|
83
|
+
await this.schemaRegistry.warmCache(this._getAllTables());
|
|
84
|
+
|
|
85
|
+
// Load Reference Data (e.g. sectors, holidays)
|
|
86
|
+
await this._loadReferenceData();
|
|
87
|
+
|
|
88
|
+
this._log('INFO', `Initialized with ${this.manifest.length} computations`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Analyze what needs to run for a given date.
|
|
93
|
+
*/
|
|
94
|
+
async analyze(options) {
|
|
95
|
+
const { date } = options;
|
|
96
|
+
if (!this.manifest) await this.initialize();
|
|
97
|
+
|
|
98
|
+
const dailyStatus = await this.stateRepository.getDailyStatus(date);
|
|
99
|
+
const prevStatus = await this.stateRepository.getDailyStatus(this._subtractDay(date));
|
|
100
|
+
|
|
101
|
+
const report = await this.runAnalyzer.analyze(date, dailyStatus, prevStatus);
|
|
102
|
+
|
|
103
|
+
// Compatibility: Merge reRuns into runnable
|
|
104
|
+
report.runnable = [...report.runnable, ...report.reRuns];
|
|
105
|
+
return report;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Main Execution Loop
|
|
110
|
+
*/
|
|
111
|
+
async execute(options) {
|
|
112
|
+
const { date, pass = null, computation = null, dryRun = false, entities = null } = options;
|
|
113
|
+
if (!this.manifest) await this.initialize();
|
|
114
|
+
|
|
115
|
+
this._log('INFO', `Starting execution for ${date}...`);
|
|
116
|
+
|
|
117
|
+
// 1. Filter Manifest
|
|
118
|
+
let toRun = this.manifest;
|
|
119
|
+
if (computation) {
|
|
120
|
+
const norm = computation.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
121
|
+
toRun = this.manifest.filter(e => e.name === norm);
|
|
122
|
+
if (!toRun.length) throw new Error(`Computation not found: ${computation}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 2. Group by Pass
|
|
126
|
+
const passes = this.manifestBuilder.groupByPass(toRun);
|
|
127
|
+
const passNumbers = Object.keys(passes).map(Number).sort((a,b) => a-b);
|
|
128
|
+
const passesToRun = pass ? [parseInt(pass, 10)] : passNumbers;
|
|
129
|
+
|
|
130
|
+
const summary = {
|
|
131
|
+
date,
|
|
132
|
+
summary: { completed: 0, skipped: 0, blocked: 0, impossible: 0, errors: 0 },
|
|
133
|
+
completed: [], skipped: [], blocked: [], impossible: [], errors: []
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// 3. Execute Passes
|
|
137
|
+
for (const passNum of passesToRun) {
|
|
138
|
+
const passComputations = passes[passNum] || [];
|
|
139
|
+
this._log('INFO', `Executing Pass ${passNum}: ${passComputations.length} computations`);
|
|
140
|
+
|
|
141
|
+
// Note: In a strict DAG, items in the same pass are parallelizable.
|
|
142
|
+
// We use Promise.all to run them concurrently.
|
|
143
|
+
await Promise.all(passComputations.map(async (entry) => {
|
|
144
|
+
try {
|
|
145
|
+
// Pass specific options like "entities" (force entities) down
|
|
146
|
+
const res = await this._executeComputation(entry, date, { dryRun, entities });
|
|
147
|
+
|
|
148
|
+
summary[res.status].push(res);
|
|
149
|
+
summary.summary[res.status]++;
|
|
150
|
+
} catch (e) {
|
|
151
|
+
summary.errors.push({ name: entry.name, error: e.message });
|
|
152
|
+
summary.summary.errors++;
|
|
153
|
+
this._log('ERROR', `${entry.name} failed: ${e.message}`);
|
|
154
|
+
}
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return summary;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async runSingle(entry, dateStr, options = {}) {
|
|
162
|
+
if (!this.manifest) await this.initialize();
|
|
163
|
+
return this._executeComputation(entry, dateStr, {
|
|
164
|
+
dryRun: options.dryRun || false,
|
|
165
|
+
entities: options.entityIds
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// =========================================================================
|
|
170
|
+
// INTERNAL EXECUTION LOGIC
|
|
171
|
+
// =========================================================================
|
|
172
|
+
|
|
173
|
+
async _executeComputation(entry, dateStr, options) {
|
|
174
|
+
const { name } = entry;
|
|
175
|
+
const forceEntities = options.entities;
|
|
176
|
+
|
|
177
|
+
// 1. Logic Check (Skip if unnecessary)
|
|
178
|
+
if (!forceEntities) {
|
|
179
|
+
const decision = await this._analyzeEntry(entry, dateStr);
|
|
180
|
+
if (decision.type !== 'runnable' && decision.type !== 'reRuns') {
|
|
181
|
+
return { name, status: decision.type, reason: decision.payload.reason };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this._log('INFO', `Running ${name} (Type: ${entry.type})...`);
|
|
186
|
+
const startTime = Date.now();
|
|
187
|
+
|
|
188
|
+
// 2. Load Dependencies & Previous Results
|
|
189
|
+
const { depResults, depResultHashes } = await this._loadDependencies(entry, dateStr);
|
|
190
|
+
|
|
191
|
+
let previousResult = null;
|
|
192
|
+
if (entry.isHistorical) {
|
|
193
|
+
previousResult = await this.stateRepository.getResult(this._subtractDay(dateStr), name);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 3. Select Execution Strategy
|
|
197
|
+
let stats = { count: 0, hash: null, skipped: false };
|
|
198
|
+
|
|
199
|
+
if (entry.type === 'per-entity' && !forceEntities) {
|
|
200
|
+
// STRATEGY A: Streaming (Low Memory, Checkpointing)
|
|
201
|
+
stats = await this._executeStreaming(entry, dateStr, depResults, previousResult, options);
|
|
202
|
+
} else {
|
|
203
|
+
// STRATEGY B: In-Memory (Global, Aggregates, or Forced Entities)
|
|
204
|
+
stats = await this._executeGlobal(entry, dateStr, depResults, previousResult, options, forceEntities);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (stats.skipped) {
|
|
208
|
+
return { name, status: 'skipped', reason: 'Results unchanged', duration: Date.now() - startTime };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 4. Update State (If real run)
|
|
212
|
+
if (!options.dryRun) {
|
|
213
|
+
await this.stateRepository.updateStatusCache(dateStr, name, {
|
|
214
|
+
hash: entry.hash,
|
|
215
|
+
resultHash: stats.hash,
|
|
216
|
+
dependencyResultHashes: depResultHashes,
|
|
217
|
+
entityCount: stats.count
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Flush any buffered lineage logs
|
|
221
|
+
await this.lineageMiddleware.flush();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { name, status: 'completed', duration: Date.now() - startTime, resultCount: stats.count };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// --- STRATEGY A: STREAMING ---
|
|
228
|
+
async _executeStreaming(entry, dateStr, depResults, previousResult, options) {
|
|
229
|
+
// 1. Setup Checkpoint
|
|
230
|
+
const checkpointer = new Checkpointer(this.config, this.storageManager);
|
|
231
|
+
let cp = null;
|
|
232
|
+
if (!options.dryRun) {
|
|
233
|
+
cp = await checkpointer.initCheckpoint(dateStr, entry.name, 0); // 0 = unknown total
|
|
234
|
+
if (cp?.isCompleted) return { count: 0, hash: 'cached', skipped: true };
|
|
235
|
+
if (cp?.isResumed) this._log('INFO', `Resuming ${entry.name} from checkpoint...`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 2. Initialize Stream
|
|
239
|
+
const batchSize = this.config.execution?.batchSize || BATCH_SIZE;
|
|
240
|
+
const batchStream = this.dataFetcher.fetchComputationBatched(entry.requires, dateStr, batchSize);
|
|
241
|
+
|
|
242
|
+
const rollingHash = crypto.createHash('sha256');
|
|
243
|
+
let totalCount = 0;
|
|
244
|
+
let batchIndex = 0;
|
|
245
|
+
|
|
246
|
+
const concurrency = this.config.execution?.entityConcurrency || DEFAULT_CONCURRENCY;
|
|
247
|
+
const limit = pLimit(concurrency);
|
|
248
|
+
|
|
249
|
+
// 3. Iterate Batches
|
|
250
|
+
for await (const batch of batchStream) {
|
|
251
|
+
// Resume Logic: Skip completed batches
|
|
252
|
+
if (cp && cp.completedBatches.has(batchIndex)) {
|
|
253
|
+
batchIndex++;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const { data: batchData, entityIds } = batch;
|
|
258
|
+
|
|
259
|
+
// 4. PREFETCH DEPENDENCIES
|
|
260
|
+
const batchDeps = await this._prefetchBatchDependencies(entry, dateStr, depResults, entityIds);
|
|
261
|
+
|
|
262
|
+
// 5. Dynamic Context Injection
|
|
263
|
+
const { rules } = this.ruleInjector.createContext(); // Used is implicit via Proxy
|
|
264
|
+
|
|
265
|
+
// 6. Execute Batch Concurrently
|
|
266
|
+
const batchResults = {};
|
|
267
|
+
|
|
268
|
+
await Promise.all(entityIds.map(entityId => limit(async () => {
|
|
269
|
+
const instance = new entry.class();
|
|
270
|
+
const entityData = this._filterDataForEntity(batchData, entityId);
|
|
271
|
+
|
|
272
|
+
const context = {
|
|
273
|
+
computation: entry,
|
|
274
|
+
date: dateStr,
|
|
275
|
+
entityId,
|
|
276
|
+
data: entityData,
|
|
277
|
+
|
|
278
|
+
// Dependency Injector
|
|
279
|
+
getDependency: (depName, targetId) => {
|
|
280
|
+
if (batchDeps[depName] && batchDeps[depName].has(targetId || entityId)) {
|
|
281
|
+
return batchDeps[depName].get(targetId || entityId);
|
|
282
|
+
}
|
|
283
|
+
return this._lazyLoadDependency(dateStr, depName, targetId || entityId, depResults);
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
previousResult,
|
|
287
|
+
rules,
|
|
288
|
+
references: this.referenceDataCache,
|
|
289
|
+
config: this.config,
|
|
290
|
+
dataFetcher: this.dataFetcher // <--- ADDED: Required by CostTrackerMiddleware
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// DELEGATE TO RUNNER
|
|
294
|
+
const result = await this.runner.run(instance, context);
|
|
295
|
+
|
|
296
|
+
if (result !== undefined) {
|
|
297
|
+
batchResults[entityId] = result;
|
|
298
|
+
this._updateRollingHash(rollingHash, result);
|
|
299
|
+
}
|
|
300
|
+
})));
|
|
301
|
+
|
|
302
|
+
// 7. Commit Batch
|
|
303
|
+
if (!options.dryRun) {
|
|
304
|
+
await this.storageManager.commitResults(dateStr, entry, batchResults, {});
|
|
305
|
+
const lastId = entityIds[entityIds.length - 1];
|
|
306
|
+
await checkpointer.markBatchComplete(dateStr, entry.name, cp?.id, batchIndex, batchSize, lastId);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
totalCount += Object.keys(batchResults).length;
|
|
310
|
+
batchIndex++;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!options.dryRun && cp) await checkpointer.complete(dateStr, entry.name, cp.id);
|
|
314
|
+
|
|
315
|
+
return { count: totalCount, hash: rollingHash.digest('hex').substring(0, 16) };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// --- STRATEGY B: GLOBAL / IN-MEMORY ---
|
|
319
|
+
async _executeGlobal(entry, dateStr, depResults, previousResult, options, forceEntities) {
|
|
320
|
+
// 1. Fetch Full Data
|
|
321
|
+
const data = await this.dataFetcher.fetchForComputation(entry.requires, dateStr, forceEntities);
|
|
322
|
+
const { rules } = this.ruleInjector.createContext();
|
|
323
|
+
|
|
324
|
+
const instance = new entry.class();
|
|
325
|
+
const context = {
|
|
326
|
+
computation: entry,
|
|
327
|
+
date: dateStr,
|
|
328
|
+
data,
|
|
329
|
+
getDependency: (dep, ent) => this._lazyLoadDependency(dateStr, dep, ent, depResults),
|
|
330
|
+
previousResult,
|
|
331
|
+
rules,
|
|
332
|
+
references: this.referenceDataCache,
|
|
333
|
+
config: this.config,
|
|
334
|
+
entityId: forceEntities ? null : '_global',
|
|
335
|
+
dataFetcher: this.dataFetcher // <--- ADDED: Required by CostTrackerMiddleware
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// 2. Delegate to Runner
|
|
339
|
+
let results = {};
|
|
340
|
+
|
|
341
|
+
if (entry.type === 'per-entity') {
|
|
342
|
+
const ids = forceEntities || this._extractEntityIds(data);
|
|
343
|
+
const limit = pLimit(DEFAULT_CONCURRENCY);
|
|
344
|
+
|
|
345
|
+
await Promise.all(ids.map(id => limit(async () => {
|
|
346
|
+
const subCtx = {
|
|
347
|
+
...context,
|
|
348
|
+
entityId: id,
|
|
349
|
+
data: this._filterDataForEntity(data, id)
|
|
350
|
+
};
|
|
351
|
+
const res = await this.runner.run(instance, subCtx);
|
|
352
|
+
if (res) results[id] = res;
|
|
353
|
+
})));
|
|
354
|
+
} else {
|
|
355
|
+
results = await this.runner.run(instance, context);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 3. Smart Invalidation Check
|
|
359
|
+
const finalHash = this._hashResults(results);
|
|
360
|
+
|
|
361
|
+
if (!options.dryRun && !forceEntities) {
|
|
362
|
+
const currentStatus = await this.stateRepository.getDailyStatus(dateStr);
|
|
363
|
+
const status = currentStatus.get(entry.name.toLowerCase());
|
|
364
|
+
|
|
365
|
+
if (status && status.resultHash === finalHash) {
|
|
366
|
+
return { count: Object.keys(results || {}).length, hash: finalHash, skipped: true };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
await this.storageManager.commitResults(dateStr, entry, results, {});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { count: Object.keys(results || {}).length, hash: finalHash };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// =========================================================================
|
|
376
|
+
// HELPER METHODS
|
|
377
|
+
// =========================================================================
|
|
378
|
+
|
|
379
|
+
async _analyzeEntry(entry, dateStr) {
|
|
380
|
+
const d = await this.stateRepository.getDailyStatus(dateStr);
|
|
381
|
+
const p = await this.stateRepository.getDailyStatus(this._subtractDay(dateStr));
|
|
382
|
+
return this.runAnalyzer._evaluateEntry(entry, dateStr, false, d, p);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async _loadDependencies(entry, dateStr) {
|
|
386
|
+
const depResults = {};
|
|
387
|
+
const depResultHashes = {};
|
|
388
|
+
const dailyStatus = await this.stateRepository.getDailyStatus(dateStr);
|
|
389
|
+
|
|
390
|
+
for (const dep of entry.dependencies) {
|
|
391
|
+
const stat = dailyStatus.get(dep);
|
|
392
|
+
if (stat?.resultHash) depResultHashes[dep] = stat.resultHash;
|
|
393
|
+
|
|
394
|
+
if (stat?.entityCount > 50000) {
|
|
395
|
+
depResults[dep] = null;
|
|
396
|
+
} else {
|
|
397
|
+
depResults[dep] = await this.stateRepository.getResult(dateStr, dep);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (entry.conditionalDependencies) {
|
|
402
|
+
for (const condDep of entry.conditionalDependencies) {
|
|
403
|
+
const shouldLoad = condDep.condition({ date: dateStr, config: this.config });
|
|
404
|
+
if (shouldLoad) {
|
|
405
|
+
const depStatus = dailyStatus.get(condDep.computation.toLowerCase());
|
|
406
|
+
if (depStatus) {
|
|
407
|
+
depResults[condDep.computation] = await this.stateRepository.getResult(dateStr, condDep.computation);
|
|
408
|
+
depResultHashes[condDep.computation] = depStatus.resultHash;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return { depResults, depResultHashes };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async _prefetchBatchDependencies(entry, dateStr, loadedDeps, batchEntityIds) {
|
|
418
|
+
const prefetched = {};
|
|
419
|
+
|
|
420
|
+
for (const depName of entry.dependencies) {
|
|
421
|
+
if (loadedDeps[depName] === null) {
|
|
422
|
+
const batchRes = await this.stateRepository.getBatchEntityResults(dateStr, depName, batchEntityIds);
|
|
423
|
+
prefetched[depName] = new Map(Object.entries(batchRes));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return prefetched;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async _lazyLoadDependency(dateStr, depName, entityId, preloaded) {
|
|
430
|
+
if (preloaded[depName] && !entityId) return preloaded[depName];
|
|
431
|
+
if (preloaded[depName] && entityId) return preloaded[depName][entityId];
|
|
432
|
+
|
|
433
|
+
if (entityId) {
|
|
434
|
+
return this.stateRepository.getEntityResult(dateStr, depName, entityId);
|
|
435
|
+
}
|
|
436
|
+
return this.stateRepository.getResult(dateStr, depName);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async _loadReferenceData() {
|
|
440
|
+
if (!this.config.referenceData) return;
|
|
441
|
+
await Promise.all(this.config.referenceData.map(async (table) => {
|
|
442
|
+
try {
|
|
443
|
+
const data = await this.dataFetcher.fetch({
|
|
444
|
+
table,
|
|
445
|
+
targetDate: new Date().toISOString().slice(0, 10),
|
|
446
|
+
mandatory: false
|
|
447
|
+
});
|
|
448
|
+
this.referenceDataCache[table] = data || {};
|
|
449
|
+
} catch (e) {
|
|
450
|
+
this._log('WARN', `Failed to load Ref Data ${table}: ${e.message}`);
|
|
451
|
+
}
|
|
452
|
+
}));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
_extractEntityIds(data) {
|
|
456
|
+
const ids = new Set();
|
|
457
|
+
Object.entries(data).forEach(([tbl, d]) => {
|
|
458
|
+
const conf = this.config.tables[tbl] || {};
|
|
459
|
+
if (conf.entityField && d && !Array.isArray(d)) Object.keys(d).forEach(k => ids.add(k));
|
|
460
|
+
});
|
|
461
|
+
return Array.from(ids);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
_filterDataForEntity(data, id) {
|
|
465
|
+
const out = {};
|
|
466
|
+
Object.entries(data).forEach(([tbl, d]) => {
|
|
467
|
+
const conf = this.config.tables[tbl] || {};
|
|
468
|
+
if (conf.entityField && d && !Array.isArray(d)) out[tbl] = d[id] || null;
|
|
469
|
+
else out[tbl] = d;
|
|
470
|
+
});
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
_updateRollingHash(hasher, result) {
|
|
475
|
+
if (result) hasher.update(JSON.stringify(result));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
_hashResults(results) {
|
|
479
|
+
const canonical = JSON.stringify(results, Object.keys(results || {}).sort());
|
|
480
|
+
return crypto.createHash('sha256').update(canonical).digest('hex').substring(0, 16);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
_subtractDay(dateStr) {
|
|
484
|
+
const d = new Date(dateStr + 'T00:00:00Z');
|
|
485
|
+
d.setUTCDate(d.getUTCDate() - 1);
|
|
486
|
+
return d.toISOString().slice(0, 10);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
_getAllTables() {
|
|
490
|
+
const s = new Set();
|
|
491
|
+
if (this.manifest) this.manifest.forEach(e => Object.keys(e.requires).forEach(t => s.add(t)));
|
|
492
|
+
return Array.from(s);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
_log(l, m) { this.logger.log(l, `[Orchestrator] ${m}`); }
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
module.exports = { Orchestrator };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Task Runner
|
|
3
|
+
* Executes a single unit of work (global or per-entity) through a middleware chain.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class TaskRunner {
|
|
7
|
+
constructor(middlewares = []) {
|
|
8
|
+
this.middlewares = middlewares;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Run the computation instance process method wrapped in middleware.
|
|
13
|
+
* @param {Object} instance - The computation class instance
|
|
14
|
+
* @param {Object} context - The execution context
|
|
15
|
+
*/
|
|
16
|
+
async run(instance, context) {
|
|
17
|
+
// The Core Kernel: The actual computation logic
|
|
18
|
+
const coreKernel = async (ctx) => {
|
|
19
|
+
await instance.process(ctx);
|
|
20
|
+
// Retrieve result based on mode
|
|
21
|
+
if (ctx.entityId) return instance.results[ctx.entityId];
|
|
22
|
+
return instance.results;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Compose Middleware: Reduce Right (Inner -> Outer)
|
|
26
|
+
const chain = this.middlewares.reduceRight((next, mw) => {
|
|
27
|
+
return async (ctx) => mw.execute(ctx, next);
|
|
28
|
+
}, coreKernel);
|
|
29
|
+
|
|
30
|
+
// Execute
|
|
31
|
+
return await chain(context);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { TaskRunner };
|
package/functions/computation-system-v2/framework/execution/middleware/CostTrackerMiddleware.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const { Middleware } = require('./Middleware');
|
|
2
|
+
const { CostTracker } = require('../../cost/CostTracker');
|
|
3
|
+
|
|
4
|
+
class CostTrackerMiddleware extends Middleware {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
super();
|
|
7
|
+
this.tracker = new CostTracker(config);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async execute(context, next) {
|
|
11
|
+
const { computation, date, dataFetcher } = context;
|
|
12
|
+
|
|
13
|
+
// Snapshot bytes before
|
|
14
|
+
const startBytes = dataFetcher.getStats().bytesProcessed || 0;
|
|
15
|
+
|
|
16
|
+
const result = await next(context);
|
|
17
|
+
|
|
18
|
+
// Snapshot bytes after
|
|
19
|
+
const endBytes = dataFetcher.getStats().bytesProcessed || 0;
|
|
20
|
+
const delta = endBytes - startBytes;
|
|
21
|
+
|
|
22
|
+
if (delta > 0) {
|
|
23
|
+
// Fire and forget
|
|
24
|
+
this.tracker.trackCost(computation.name, date, delta)
|
|
25
|
+
.catch(e => console.error('Cost tracking failed', e));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { CostTrackerMiddleware };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const { Middleware } = require('./Middleware');
|
|
2
|
+
const { LineageTracker } = require('../../lineage/LineageTracker');
|
|
3
|
+
|
|
4
|
+
class LineageMiddleware extends Middleware {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
super();
|
|
7
|
+
this.tracker = new LineageTracker(config);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async execute(context, next) {
|
|
11
|
+
const result = await next(context);
|
|
12
|
+
|
|
13
|
+
// Only track if we have a valid result and entity
|
|
14
|
+
if (result && context.entityId) {
|
|
15
|
+
this.tracker.track({
|
|
16
|
+
computation: context.computation.name,
|
|
17
|
+
date: context.date,
|
|
18
|
+
entityId: context.entityId,
|
|
19
|
+
sourceData: context.data, // The slice of data used
|
|
20
|
+
result: result
|
|
21
|
+
}).catch(e => console.error('Lineage tracking failed', e));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async flush() {
|
|
28
|
+
await this.tracker.flush();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { LineageMiddleware };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Middleware Interface
|
|
3
|
+
*/
|
|
4
|
+
class Middleware {
|
|
5
|
+
/**
|
|
6
|
+
* @param {Object} context - The execution context
|
|
7
|
+
* @param {Function} next - The next function in the chain
|
|
8
|
+
*/
|
|
9
|
+
async execute(context, next) {
|
|
10
|
+
return await next(context);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { Middleware };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const { Middleware } = require('./Middleware');
|
|
2
|
+
const { ComputationProfiler } = require('../../monitoring/Profiler');
|
|
3
|
+
|
|
4
|
+
class ProfilerMiddleware extends Middleware {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
super();
|
|
7
|
+
this.profiler = new ComputationProfiler();
|
|
8
|
+
this.storageManager = null; // Injected by Orchestrator
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
setStorage(storageManager) {
|
|
12
|
+
this.storageManager = storageManager;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async execute(context, next) {
|
|
16
|
+
const { computation, entityId, date } = context;
|
|
17
|
+
|
|
18
|
+
// Start Profile
|
|
19
|
+
const key = this.profiler.startProfile(computation.name, entityId || 'global');
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Run Next
|
|
23
|
+
const result = await next(context);
|
|
24
|
+
return result;
|
|
25
|
+
} finally {
|
|
26
|
+
// End Profile (runs even if error)
|
|
27
|
+
const resultSize = context.results ? JSON.stringify(context.results).length : 0;
|
|
28
|
+
const profile = this.profiler.endProfile(key, {
|
|
29
|
+
entityId: entityId || 'global',
|
|
30
|
+
resultSize
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Persist Profile if storage available
|
|
34
|
+
if (this.storageManager && profile) {
|
|
35
|
+
// Async save (don't block)
|
|
36
|
+
this.storageManager.savePerformanceReport(date, {
|
|
37
|
+
computations: [{
|
|
38
|
+
name: computation.name,
|
|
39
|
+
...profile
|
|
40
|
+
}]
|
|
41
|
+
}).catch(err => console.error('Failed to save profile', err));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { ProfilerMiddleware };
|