bulltrackers-module 1.0.296 → 1.0.298
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/executors/StandardExecutor.js +44 -86
- package/functions/computation-system/helpers/computation_worker.js +40 -16
- package/functions/computation-system/persistence/ResultCommitter.js +77 -183
- package/functions/computation-system/persistence/RunRecorder.js +20 -10
- package/functions/computation-system/utils/utils.js +66 -84
- package/functions/generic-api/admin-api/index.js +151 -4
- package/package.json +1 -1
|
@@ -6,12 +6,14 @@ const { FieldValue, FieldPath } = require('@google-cloud/firestore');
|
|
|
6
6
|
const crypto = require('crypto');
|
|
7
7
|
|
|
8
8
|
// [NEW] Single Source of Truth for Data Availability
|
|
9
|
+
// These are defaults/fallbacks. The dynamic function getEarliestDataDates below is the primary source.
|
|
9
10
|
const DEFINITIVE_EARLIEST_DATES = {
|
|
10
|
-
portfolio: new Date('2025-
|
|
11
|
-
history: new Date('2025-
|
|
12
|
-
social: new Date('2025-
|
|
13
|
-
insights: new Date('2025-08-
|
|
14
|
-
price: new Date('2025-08-01T00:00:00Z')
|
|
11
|
+
portfolio: new Date('2025-01-01T00:00:00Z'),
|
|
12
|
+
history: new Date('2025-08-01T00:00:00Z'),
|
|
13
|
+
social: new Date('2025-08-01T00:00:00Z'),
|
|
14
|
+
insights: new Date('2025-08-01T00:00:00Z'),
|
|
15
|
+
price: new Date('2025-08-01T00:00:00Z'),
|
|
16
|
+
absoluteEarliest: new Date('2023-08-01T00:00:00Z')
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
/** Stage 1: Normalize a calculation name to kebab-case */
|
|
@@ -162,95 +164,75 @@ function getExpectedDateStrings(startDate, endDate) {
|
|
|
162
164
|
return dateStrings;
|
|
163
165
|
}
|
|
164
166
|
|
|
165
|
-
/** Stage 4: Get the earliest date
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const query = db.collection(collectionName).where(FieldPath.documentId(), '>=', '2000-01-01').orderBy(FieldPath.documentId(), 'asc').limit(1);
|
|
171
|
-
const snapshot = await withRetry(() => query.get(), `GetEarliestDoc(${collectionName})`);
|
|
172
|
-
if (!snapshot.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshot.docs[0].id)) { return new Date(snapshot.docs[0].id + 'T00:00:00Z'); }
|
|
173
|
-
} catch (e) { logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message }); }
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Stage 4: Get the earliest date in a sharded collection */
|
|
178
|
-
async function getFirstDateFromCollection(config, deps, collectionName) {
|
|
167
|
+
/** * Stage 4: Get the earliest date from the Centralized Root Data Index.
|
|
168
|
+
* This REPLACES the expensive/error-prone raw collection scanning.
|
|
169
|
+
* It queries the 'system_root_data_index' to find the first date where data flags are TRUE.
|
|
170
|
+
*/
|
|
171
|
+
async function getEarliestDataDates(config, deps) {
|
|
179
172
|
const { db, logger } = deps;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
173
|
+
// Default to 'system_root_data_index' if not configured
|
|
174
|
+
const indexCollection = process.env.ROOT_DATA_AVAILABILITY_COLLECTION ||
|
|
175
|
+
config.rootDataAvailabilityCollection ||
|
|
176
|
+
'system_root_data_index';
|
|
177
|
+
|
|
178
|
+
// Helper to find earliest date where a specific flag is true
|
|
179
|
+
// Efficient: Uses the document ID (date string) index.
|
|
180
|
+
const getEarliestForType = async (flagName) => {
|
|
181
|
+
try {
|
|
182
|
+
const snapshot = await db.collection(indexCollection)
|
|
183
|
+
.where(flagName, '==', true)
|
|
184
|
+
.orderBy(FieldPath.documentId(), 'asc')
|
|
185
|
+
.limit(1)
|
|
186
|
+
.get();
|
|
187
|
+
|
|
188
|
+
if (!snapshot.empty) {
|
|
189
|
+
const dateStr = snapshot.docs[0].id; // YYYY-MM-DD
|
|
190
|
+
// Safety check on date format
|
|
191
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
|
192
|
+
return new Date(dateStr + 'T00:00:00Z');
|
|
193
|
+
}
|
|
191
194
|
}
|
|
195
|
+
} catch (e) {
|
|
196
|
+
logger.log('WARN', `[Utils] Failed to query index for ${flagName}: ${e.message}`);
|
|
192
197
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
198
|
+
return null;
|
|
199
|
+
};
|
|
196
200
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
getFirstDateFromCollection (config, deps, config.speculatorHistoryCollection),
|
|
205
|
-
getFirstDateFromSimpleCollection (config, deps, config.insightsCollectionName),
|
|
206
|
-
getFirstDateFromSimpleCollection (config, deps, config.socialInsightsCollectionName),
|
|
207
|
-
getFirstDateFromPriceCollection (config, deps)
|
|
201
|
+
// Parallel query for all data types
|
|
202
|
+
const [portfolioDate, historyDate, socialDate, insightsDate, priceDate] = await Promise.all([
|
|
203
|
+
getEarliestForType('hasPortfolio'),
|
|
204
|
+
getEarliestForType('hasHistory'),
|
|
205
|
+
getEarliestForType('hasSocial'),
|
|
206
|
+
getEarliestForType('hasInsights'),
|
|
207
|
+
getEarliestForType('hasPrices')
|
|
208
208
|
]);
|
|
209
209
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
210
|
+
// Calculate absolute earliest among found dates
|
|
211
|
+
const foundDates = [portfolioDate, historyDate, socialDate, insightsDate, priceDate].filter(d => d !== null);
|
|
212
|
+
|
|
213
|
+
let absoluteEarliest = null;
|
|
214
|
+
if (foundDates.length > 0) {
|
|
215
|
+
absoluteEarliest = new Date(Math.min(...foundDates));
|
|
216
|
+
} else {
|
|
217
|
+
// Fallback if index is empty
|
|
218
|
+
const configStart = config.earliestComputationDate || '2023-01-01';
|
|
219
|
+
absoluteEarliest = new Date(configStart + 'T00:00:00Z');
|
|
220
|
+
logger.log('WARN', `[Utils] No data found in Root Data Index (${indexCollection}). Defaulting to ${configStart}`);
|
|
221
|
+
}
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
// Update the static export for consumers who use it (best effort synchronization)
|
|
224
|
+
DEFINITIVE_EARLIEST_DATES.absoluteEarliest = absoluteEarliest;
|
|
224
225
|
|
|
225
226
|
return {
|
|
226
|
-
portfolio:
|
|
227
|
-
history:
|
|
228
|
-
insights:
|
|
229
|
-
social:
|
|
230
|
-
price:
|
|
231
|
-
absoluteEarliest: absoluteEarliest
|
|
227
|
+
portfolio: portfolioDate || new Date('2999-12-31T00:00:00Z'),
|
|
228
|
+
history: historyDate || new Date('2999-12-31T00:00:00Z'),
|
|
229
|
+
insights: insightsDate || new Date('2999-12-31T00:00:00Z'),
|
|
230
|
+
social: socialDate || new Date('2999-12-31T00:00:00Z'),
|
|
231
|
+
price: priceDate || new Date('2999-12-31T00:00:00Z'),
|
|
232
|
+
absoluteEarliest: absoluteEarliest
|
|
232
233
|
};
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
async function getFirstDateFromPriceCollection(config, deps) {
|
|
236
|
-
const { db, logger } = deps;
|
|
237
|
-
const collection = config.priceCollection || 'asset_prices';
|
|
238
|
-
try {
|
|
239
|
-
const snapshot = await withRetry(() => db.collection(collection).limit(10).get(), `GetPriceShards(${collection})`);
|
|
240
|
-
let earliestDate = null;
|
|
241
|
-
snapshot.forEach(doc => {
|
|
242
|
-
const shardData = doc.data();
|
|
243
|
-
for (const instrumentId in shardData) {
|
|
244
|
-
const instrumentData = shardData[instrumentId];
|
|
245
|
-
if (!instrumentData.prices) continue;
|
|
246
|
-
const dates = Object.keys(instrumentData.prices).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort();
|
|
247
|
-
if (dates.length > 0) { const firstDate = new Date(dates[0] + 'T00:00:00Z'); if (!earliestDate || firstDate < earliestDate) earliestDate = firstDate; }
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
return earliestDate;
|
|
251
|
-
} catch (e) { logger.log('ERROR', `Failed to get earliest price date from ${collection}`, { errorMessage: e.message }); return null; }
|
|
252
|
-
}
|
|
253
|
-
|
|
254
236
|
module.exports = {
|
|
255
237
|
FieldValue,
|
|
256
238
|
FieldPath,
|
|
@@ -259,7 +241,7 @@ module.exports = {
|
|
|
259
241
|
getExpectedDateStrings,
|
|
260
242
|
getEarliestDataDates,
|
|
261
243
|
generateCodeHash,
|
|
262
|
-
generateDataHash,
|
|
244
|
+
generateDataHash,
|
|
263
245
|
withRetry,
|
|
264
246
|
DEFINITIVE_EARLIEST_DATES
|
|
265
|
-
};
|
|
247
|
+
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @fileoverview Admin API Router
|
|
3
3
|
* Sub-module for system observability, debugging, and visualization.
|
|
4
4
|
* Mounted at /admin within the Generic API.
|
|
5
|
+
* UPDATED: Added advanced cost, performance, and live monitoring endpoints.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
const express = require('express');
|
|
@@ -84,8 +85,6 @@ const createAdminRouter = (config, dependencies, unifiedCalculations) => {
|
|
|
84
85
|
});
|
|
85
86
|
|
|
86
87
|
// --- 2. STATUS MATRIX (Calendar / State UI) ---
|
|
87
|
-
// Returns status of ALL computations across a date range.
|
|
88
|
-
// ENHANCED: Cross-references Manifest to detect "PENDING" (Not run yet) vs "MISSING".
|
|
89
88
|
router.get('/matrix', async (req, res) => {
|
|
90
89
|
const { start, end } = req.query;
|
|
91
90
|
if (!start || !end) return res.status(400).json({ error: "Start and End dates required." });
|
|
@@ -153,7 +152,6 @@ const createAdminRouter = (config, dependencies, unifiedCalculations) => {
|
|
|
153
152
|
});
|
|
154
153
|
|
|
155
154
|
// --- 3. PIPELINE STATE (Progress Bar) ---
|
|
156
|
-
// Shows realtime status of the 5-pass system for a specific date
|
|
157
155
|
router.get('/pipeline/state', async (req, res) => {
|
|
158
156
|
const { date } = req.query;
|
|
159
157
|
if (!date) return res.status(400).json({ error: "Date required" });
|
|
@@ -352,7 +350,6 @@ const createAdminRouter = (config, dependencies, unifiedCalculations) => {
|
|
|
352
350
|
});
|
|
353
351
|
|
|
354
352
|
// --- 7. FLIGHT RECORDER (Inspection) ---
|
|
355
|
-
// Existing inspection endpoint kept for drill-down
|
|
356
353
|
router.get('/inspect/:date/:calcName', async (req, res) => {
|
|
357
354
|
const { date, calcName } = req.params;
|
|
358
355
|
try {
|
|
@@ -379,6 +376,156 @@ const createAdminRouter = (config, dependencies, unifiedCalculations) => {
|
|
|
379
376
|
}
|
|
380
377
|
});
|
|
381
378
|
|
|
379
|
+
// --- 8. COST & RESOURCE ANALYSIS ---
|
|
380
|
+
router.get('/analytics/costs', async (req, res) => {
|
|
381
|
+
const { date, days } = req.query;
|
|
382
|
+
// Default to today if no date, or range if days provided
|
|
383
|
+
const targetDate = date || new Date().toISOString().slice(0, 10);
|
|
384
|
+
|
|
385
|
+
// Simple Cost Model (Estimates)
|
|
386
|
+
const COSTS = {
|
|
387
|
+
write: 0.18 / 100000,
|
|
388
|
+
read: 0.06 / 100000,
|
|
389
|
+
delete: 0.02 / 100000,
|
|
390
|
+
compute_std_sec: 0.000023, // 1vCPU 2GB (approx)
|
|
391
|
+
compute_high_sec: 0.000092 // 2vCPU 8GB (approx)
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const auditRef = db.collection('computation_audit_logs');
|
|
396
|
+
// We scan the 'history' subcollectionGroup for the given date(s)
|
|
397
|
+
// Note: This can be expensive. In prod, you'd want aggregate counters.
|
|
398
|
+
const query = db.collectionGroup('history').where('targetDate', '==', targetDate);
|
|
399
|
+
const snap = await query.get();
|
|
400
|
+
|
|
401
|
+
let totalCost = 0;
|
|
402
|
+
const byPass = {};
|
|
403
|
+
const byCalc = {};
|
|
404
|
+
|
|
405
|
+
snap.forEach(doc => {
|
|
406
|
+
const data = doc.data();
|
|
407
|
+
const ops = data.firestoreOps || { reads: 0, writes: 0, deletes: 0 };
|
|
408
|
+
const durationSec = (data.durationMs || 0) / 1000;
|
|
409
|
+
const tier = data.resourceTier || 'standard';
|
|
410
|
+
|
|
411
|
+
const ioCost = (ops.writes * COSTS.write) + (ops.reads * COSTS.read) + (ops.deletes * COSTS.delete);
|
|
412
|
+
const computeCost = durationSec * (tier === 'high-mem' ? COSTS.compute_high_sec : COSTS.compute_std_sec);
|
|
413
|
+
const itemCost = ioCost + computeCost;
|
|
414
|
+
|
|
415
|
+
totalCost += itemCost;
|
|
416
|
+
|
|
417
|
+
// Aggregations
|
|
418
|
+
const pass = data.pass || 'unknown';
|
|
419
|
+
if (!byPass[pass]) byPass[pass] = { cost: 0, runs: 0, duration: 0 };
|
|
420
|
+
byPass[pass].cost += itemCost;
|
|
421
|
+
byPass[pass].runs++;
|
|
422
|
+
byPass[pass].duration += durationSec;
|
|
423
|
+
|
|
424
|
+
const calc = data.computationName;
|
|
425
|
+
if (!byCalc[calc]) byCalc[calc] = { cost: 0, runs: 0, ops: { r:0, w:0 } };
|
|
426
|
+
byCalc[calc].cost += itemCost;
|
|
427
|
+
byCalc[calc].runs++;
|
|
428
|
+
byCalc[calc].ops.r += ops.reads;
|
|
429
|
+
byCalc[calc].ops.w += ops.writes;
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Top 10 Expensive Calcs
|
|
433
|
+
const topCalcs = Object.entries(byCalc)
|
|
434
|
+
.sort((a, b) => b[1].cost - a[1].cost)
|
|
435
|
+
.slice(0, 10)
|
|
436
|
+
.map(([name, stats]) => ({ name, ...stats }));
|
|
437
|
+
|
|
438
|
+
res.json({
|
|
439
|
+
date: targetDate,
|
|
440
|
+
totalCostUSD: totalCost,
|
|
441
|
+
breakdown: {
|
|
442
|
+
byPass,
|
|
443
|
+
topCalculations: topCalcs
|
|
444
|
+
},
|
|
445
|
+
meta: { model: COSTS }
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// --- 9. REROUTE (OOM) ANALYSIS ---
|
|
452
|
+
router.get('/analytics/reroutes', async (req, res) => {
|
|
453
|
+
const { date } = req.query;
|
|
454
|
+
if (!date) return res.status(400).json({ error: "Date required" });
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
// Find all runs that used high-mem
|
|
458
|
+
const query = db.collectionGroup('history')
|
|
459
|
+
.where('targetDate', '==', date)
|
|
460
|
+
.where('resourceTier', '==', 'high-mem');
|
|
461
|
+
|
|
462
|
+
const snap = await query.get();
|
|
463
|
+
const reroutes = [];
|
|
464
|
+
|
|
465
|
+
snap.forEach(doc => {
|
|
466
|
+
const data = doc.data();
|
|
467
|
+
reroutes.push({
|
|
468
|
+
computation: data.computationName,
|
|
469
|
+
pass: data.pass,
|
|
470
|
+
trigger: data.trigger?.reason,
|
|
471
|
+
peakMemoryMB: data.peakMemoryMB,
|
|
472
|
+
durationMs: data.durationMs,
|
|
473
|
+
runId: data.runId
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
res.json({ count: reroutes.length, reroutes });
|
|
478
|
+
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// --- 10. LIVE DASHBOARD (Snapshot) ---
|
|
482
|
+
// Poll this endpoint to simulate a WebSocket feed
|
|
483
|
+
router.get('/live/dashboard', async (req, res) => {
|
|
484
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
485
|
+
try {
|
|
486
|
+
// Query the Ledger for Active Tasks
|
|
487
|
+
// We look at all passes for today
|
|
488
|
+
const passes = ['1', '2', '3', '4', '5'];
|
|
489
|
+
const activeTasks = [];
|
|
490
|
+
const recentFailures = [];
|
|
491
|
+
|
|
492
|
+
await Promise.all(passes.map(async (pass) => {
|
|
493
|
+
const colRef = db.collection(`computation_audit_ledger/${today}/passes/${pass}/tasks`);
|
|
494
|
+
|
|
495
|
+
// Get Running
|
|
496
|
+
const runningSnap = await colRef.where('status', 'in', ['PENDING', 'IN_PROGRESS']).get();
|
|
497
|
+
runningSnap.forEach(doc => {
|
|
498
|
+
activeTasks.push({ pass, ...doc.data() });
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Get Recent Failures (last 10 mins?? hard to query without index, just grab failures)
|
|
502
|
+
const failSnap = await colRef.where('status', '==', 'FAILED').get();
|
|
503
|
+
failSnap.forEach(doc => {
|
|
504
|
+
recentFailures.push({ pass, ...doc.data() });
|
|
505
|
+
});
|
|
506
|
+
}));
|
|
507
|
+
|
|
508
|
+
// Get Pipeline Stage (which pass is active?)
|
|
509
|
+
// We infer this by seeing which pass has pending tasks
|
|
510
|
+
let currentStage = 'IDLE';
|
|
511
|
+
for (const p of passes) {
|
|
512
|
+
const hasActive = activeTasks.some(t => t.pass === p);
|
|
513
|
+
if (hasActive) { currentStage = `PASS_${p}`; break; }
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
res.json({
|
|
517
|
+
status: 'success',
|
|
518
|
+
timestamp: new Date(),
|
|
519
|
+
pipelineState: currentStage,
|
|
520
|
+
activeCount: activeTasks.length,
|
|
521
|
+
failureCount: recentFailures.length,
|
|
522
|
+
tasks: activeTasks,
|
|
523
|
+
failures: recentFailures
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
527
|
+
});
|
|
528
|
+
|
|
382
529
|
return router;
|
|
383
530
|
};
|
|
384
531
|
|