bulltrackers-module 1.0.277 → 1.0.279
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.
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @fileoverview Handles saving computation results with observability and Smart Cleanup.
|
|
3
3
|
* UPDATED: Implements GZIP Compression for efficient storage.
|
|
4
4
|
* UPDATED: Implements Content-Based Hashing (ResultHash) for dependency short-circuiting.
|
|
5
|
+
* UPDATED: Auto-enforces Weekend Mode validation for 'Price-Only' computations.
|
|
5
6
|
*/
|
|
6
7
|
const { commitBatchInChunks, generateDataHash } = require('../utils/utils');
|
|
7
8
|
const { updateComputationStatus } = require('./StatusRepository');
|
|
@@ -10,7 +11,7 @@ const { generateProcessId, PROCESS_TYPES } = require('../logger/logger');
|
|
|
10
11
|
const { HeuristicValidator } = require('./ResultsValidator');
|
|
11
12
|
const validationOverrides = require('../config/validation_overrides');
|
|
12
13
|
const pLimit = require('p-limit');
|
|
13
|
-
const zlib = require('zlib');
|
|
14
|
+
const zlib = require('zlib');
|
|
14
15
|
|
|
15
16
|
const NON_RETRYABLE_ERRORS = [
|
|
16
17
|
'PERMISSION_DENIED', 'DATA_LOSS', 'FAILED_PRECONDITION'
|
|
@@ -28,30 +29,50 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
28
29
|
const pid = generateProcessId(PROCESS_TYPES.STORAGE, passName, dStr);
|
|
29
30
|
|
|
30
31
|
// Options defaults
|
|
31
|
-
const flushMode
|
|
32
|
-
const shardIndexes
|
|
32
|
+
const flushMode = options.flushMode || 'STANDARD';
|
|
33
|
+
const shardIndexes = options.shardIndexes || {};
|
|
33
34
|
const nextShardIndexes = {};
|
|
34
35
|
|
|
35
36
|
const fanOutLimit = pLimit(10);
|
|
36
37
|
|
|
37
38
|
for (const name in stateObj) {
|
|
38
|
-
const calc
|
|
39
|
+
const calc = stateObj[name];
|
|
39
40
|
const execStats = calc._executionStats || { processedUsers: 0, skippedUsers: 0 };
|
|
40
41
|
const currentShardIndex = shardIndexes[name] || 0;
|
|
41
42
|
|
|
42
43
|
const runMetrics = {
|
|
43
|
-
storage:
|
|
44
|
+
storage: { sizeBytes: 0, isSharded: false, shardCount: 1, keys: 0 },
|
|
44
45
|
validation: { isValid: true, anomalies: [] },
|
|
45
46
|
execution: execStats
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
try {
|
|
49
50
|
const result = await calc.getResult();
|
|
50
|
-
const
|
|
51
|
+
const configOverrides = validationOverrides[calc.manifest.name] || {};
|
|
51
52
|
|
|
53
|
+
// --- [NEW] AUTO-ENFORCE WEEKEND MODE FOR PRICE-ONLY CALCS ---
|
|
54
|
+
// If a calculation depends SOLELY on 'price', we assume market closures
|
|
55
|
+
// will cause 0s/Flatlines on weekends, so we enforce lenient validation.
|
|
56
|
+
const dataDeps = calc.manifest.rootDataDependencies || [];
|
|
57
|
+
const isPriceOnly = (dataDeps.length === 1 && dataDeps[0] === 'price');
|
|
58
|
+
|
|
59
|
+
let effectiveOverrides = { ...configOverrides };
|
|
60
|
+
|
|
61
|
+
if (isPriceOnly && !effectiveOverrides.weekend) {
|
|
62
|
+
// Apply strict leniency for weekend/holiday price actions
|
|
63
|
+
effectiveOverrides.weekend = {
|
|
64
|
+
maxZeroPct: 100,
|
|
65
|
+
maxFlatlinePct: 100,
|
|
66
|
+
maxNullPct: 100
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// -----------------------------------------------------------
|
|
70
|
+
|
|
52
71
|
// Validation
|
|
53
72
|
if (result && Object.keys(result).length > 0) {
|
|
54
|
-
|
|
73
|
+
// [FIX] Added 'dStr' as 3rd argument to match HeuristicValidator signature
|
|
74
|
+
const healthCheck = HeuristicValidator.analyze(calc.manifest.name, result, dStr, effectiveOverrides);
|
|
75
|
+
|
|
55
76
|
if (!healthCheck.valid) {
|
|
56
77
|
runMetrics.validation.isValid = false;
|
|
57
78
|
runMetrics.validation.anomalies.push(healthCheck.reason);
|
|
@@ -91,7 +112,7 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
91
112
|
const isMultiDate = resultKeys.length > 0 && resultKeys.every(k => /^\d{4}-\d{2}-\d{2}$/.test(k));
|
|
92
113
|
|
|
93
114
|
if (isMultiDate) {
|
|
94
|
-
const datePromises
|
|
115
|
+
const datePromises = resultKeys.map((historicalDate) => fanOutLimit(async () => {
|
|
95
116
|
const dailyData = result[historicalDate];
|
|
96
117
|
if (!dailyData || Object.keys(dailyData).length === 0) return;
|
|
97
118
|
|
|
@@ -173,17 +194,14 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
173
194
|
|
|
174
195
|
async function writeSingleResult(result, docRef, name, dateContext, logger, config, deps, startShardIndex = 0, flushMode = 'STANDARD') {
|
|
175
196
|
|
|
176
|
-
// ---
|
|
177
|
-
// Try to compress before falling back to complex sharding
|
|
197
|
+
// --- COMPRESSION STRATEGY ---
|
|
178
198
|
try {
|
|
179
199
|
const jsonString = JSON.stringify(result);
|
|
180
200
|
const rawBuffer = Buffer.from(jsonString);
|
|
181
201
|
|
|
182
|
-
// Only attempt if meaningful size (> 50KB)
|
|
183
202
|
if (rawBuffer.length > 50 * 1024) {
|
|
184
203
|
const compressedBuffer = zlib.gzipSync(rawBuffer);
|
|
185
204
|
|
|
186
|
-
// If compressed fits in one document (< 900KB safety limit)
|
|
187
205
|
if (compressedBuffer.length < 900 * 1024) {
|
|
188
206
|
logger.log('INFO', `[Compression] ${name}: Compressed ${(rawBuffer.length/1024).toFixed(0)}KB -> ${(compressedBuffer.length/1024).toFixed(0)}KB. Saved as Blob.`);
|
|
189
207
|
|
|
@@ -194,7 +212,6 @@ async function writeSingleResult(result, docRef, name, dateContext, logger, conf
|
|
|
194
212
|
payload: compressedBuffer
|
|
195
213
|
};
|
|
196
214
|
|
|
197
|
-
// Write immediately
|
|
198
215
|
await docRef.set(compressedPayload, { merge: true });
|
|
199
216
|
|
|
200
217
|
return {
|
|
@@ -24,11 +24,11 @@ class HeuristicValidator {
|
|
|
24
24
|
const sampleSize = Math.min(totalItems, 100);
|
|
25
25
|
const step = Math.floor(totalItems / sampleSize);
|
|
26
26
|
|
|
27
|
-
let zeroCount
|
|
28
|
-
let nullCount
|
|
29
|
-
let nanCount
|
|
27
|
+
let zeroCount = 0;
|
|
28
|
+
let nullCount = 0;
|
|
29
|
+
let nanCount = 0;
|
|
30
30
|
let emptyVectorCount = 0;
|
|
31
|
-
let analyzedCount
|
|
31
|
+
let analyzedCount = 0;
|
|
32
32
|
|
|
33
33
|
const numericValues = [];
|
|
34
34
|
|
|
@@ -82,9 +82,9 @@ class HeuristicValidator {
|
|
|
82
82
|
|
|
83
83
|
// Default Thresholds
|
|
84
84
|
let thresholds = {
|
|
85
|
-
maxZeroPct:
|
|
86
|
-
maxNullPct:
|
|
87
|
-
maxNanPct:
|
|
85
|
+
maxZeroPct: overrides.maxZeroPct ?? 99,
|
|
86
|
+
maxNullPct: overrides.maxNullPct ?? 90,
|
|
87
|
+
maxNanPct: overrides.maxNanPct ?? 0,
|
|
88
88
|
maxFlatlinePct: overrides.maxFlatlinePct ?? 95
|
|
89
89
|
};
|
|
90
90
|
|
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
* @fileoverview Build Reporter & Auto-Runner.
|
|
3
3
|
* Generates a "Pre-Flight" report of what the computation system WILL do.
|
|
4
4
|
* REFACTORED: Strict 5-category reporting with date-based exclusion logic.
|
|
5
|
-
* UPDATED:
|
|
6
|
-
*
|
|
5
|
+
* UPDATED: Replaced Batch Writes with Parallel Writes to prevent DEADLINE_EXCEEDED timeouts.
|
|
6
|
+
* FIXED: Ensures 'latest' pointer updates even if detail writes fail.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const { analyzeDateExecution } = require('../WorkflowOrchestrator');
|
|
10
10
|
const { fetchComputationStatus } = require('../persistence/StatusRepository');
|
|
11
11
|
const { normalizeName, getExpectedDateStrings, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils');
|
|
12
12
|
const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
|
|
13
|
-
const { commitBatchInChunks } = require('../persistence/FirestoreUtils');
|
|
14
13
|
const pLimit = require('p-limit');
|
|
15
14
|
const path = require('path');
|
|
16
15
|
const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
|
|
@@ -44,8 +43,7 @@ function isDateBeforeAvailability(dateStr, calcManifest) {
|
|
|
44
43
|
|
|
45
44
|
/**
|
|
46
45
|
* AUTO-RUN ENTRY POINT
|
|
47
|
-
*
|
|
48
|
-
* If we deploy multiple computation pass nodes simultaneously, only one should run the report.
|
|
46
|
+
* Uses transactional locking to prevent race conditions.
|
|
49
47
|
*/
|
|
50
48
|
async function ensureBuildReport(config, dependencies, manifest) {
|
|
51
49
|
const { db, logger } = dependencies;
|
|
@@ -87,7 +85,7 @@ async function ensureBuildReport(config, dependencies, manifest) {
|
|
|
87
85
|
}
|
|
88
86
|
|
|
89
87
|
/**
|
|
90
|
-
* Generates the report and saves to Firestore
|
|
88
|
+
* Generates the report and saves to Firestore.
|
|
91
89
|
*/
|
|
92
90
|
async function generateBuildReport(config, dependencies, manifest, daysBack = 90, customBuildId = null) {
|
|
93
91
|
const { db, logger } = dependencies;
|
|
@@ -152,7 +150,7 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
152
150
|
impossible: [], // Missing Data (Historical) / Impossible Dependency
|
|
153
151
|
uptodate: [], // Hash Match (Previously "Skipped")
|
|
154
152
|
|
|
155
|
-
//
|
|
153
|
+
// Metadata for Verification
|
|
156
154
|
meta: {
|
|
157
155
|
totalIncluded: 0,
|
|
158
156
|
totalExpected: 0,
|
|
@@ -165,7 +163,6 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
165
163
|
dateSummary.meta.totalExpected = expectedCount;
|
|
166
164
|
|
|
167
165
|
// Helper to push only if date is valid for this specific calc
|
|
168
|
-
// [UPDATED] Adds 'pass' number to the record
|
|
169
166
|
const pushIfValid = (targetArray, item, extraReason = null) => {
|
|
170
167
|
const calcManifest = manifestMap.get(item.name);
|
|
171
168
|
if (calcManifest && isDateBeforeAvailability(dateStr, calcManifest)) {
|
|
@@ -186,7 +183,6 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
186
183
|
analysis.reRuns.forEach(item => pushIfValid(dateSummary.rerun, item, "Hash Mismatch"));
|
|
187
184
|
|
|
188
185
|
// 3. BLOCKED (Temporary Issues)
|
|
189
|
-
// Merging 'blocked' and 'failedDependency' as both are temporary blocks
|
|
190
186
|
analysis.blocked.forEach(item => pushIfValid(dateSummary.blocked, item));
|
|
191
187
|
analysis.failedDependency.forEach(item => pushIfValid(dateSummary.blocked, item, "Dependency Missing"));
|
|
192
188
|
|
|
@@ -210,7 +206,7 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
210
206
|
logger.log('WARN', `[BuildReporter] ⚠️ Mismatch on ${dateStr}: Expected ${expectedCount} but got ${includedCount}.`);
|
|
211
207
|
}
|
|
212
208
|
|
|
213
|
-
//
|
|
209
|
+
// QUEUE THE WRITE (Don't write yet)
|
|
214
210
|
const detailRef = db.collection('computation_build_records').doc(buildId).collection('details').doc(dateStr);
|
|
215
211
|
detailWrites.push({
|
|
216
212
|
ref: detailRef,
|
|
@@ -242,17 +238,50 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
242
238
|
scanRange: `${datesToCheck[0]} to ${datesToCheck[datesToCheck.length-1]}`
|
|
243
239
|
};
|
|
244
240
|
|
|
241
|
+
// 1. Write the main report header (Specific Version)
|
|
245
242
|
const reportRef = db.collection('computation_build_records').doc(buildId);
|
|
246
243
|
await reportRef.set(reportHeader);
|
|
247
244
|
|
|
245
|
+
// 2. Write Details (Protected & Parallelized)
|
|
246
|
+
// FIX: Using parallel individual writes instead of Batch to avoid DEADLINE_EXCEEDED
|
|
247
|
+
let detailsSuccess = true;
|
|
248
248
|
if (detailWrites.length > 0) {
|
|
249
|
-
logger.log('INFO', `[BuildReporter] Writing ${detailWrites.length} detail records...`);
|
|
250
|
-
|
|
251
|
-
|
|
249
|
+
logger.log('INFO', `[BuildReporter] Writing ${detailWrites.length} detail records (Parallel Strategy)...`);
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
// Concurrency limit of 15 to be safe
|
|
253
|
+
const writeLimit = pLimit(15);
|
|
254
|
+
const writePromises = detailWrites.map(w => writeLimit(() =>
|
|
255
|
+
w.ref.set(w.data).catch(e => {
|
|
256
|
+
logger.log('WARN', `[BuildReporter] Failed to write detail for ${w.ref.path}: ${e.message}`);
|
|
257
|
+
throw e;
|
|
258
|
+
})
|
|
259
|
+
));
|
|
260
|
+
|
|
261
|
+
await Promise.all(writePromises);
|
|
262
|
+
logger.log('INFO', `[BuildReporter] Successfully wrote all detail records.`);
|
|
252
263
|
|
|
253
|
-
|
|
264
|
+
} catch (detailErr) {
|
|
265
|
+
detailsSuccess = false;
|
|
266
|
+
logger.log('ERROR', `[BuildReporter] ⚠️ Failed to write some details. Report Header is preserved.`, detailErr);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
254
269
|
|
|
255
|
-
|
|
270
|
+
// 3. Update 'latest' pointer
|
|
271
|
+
// This runs regardless of detail write success/failure
|
|
272
|
+
const latestMetadata = {
|
|
273
|
+
...reportHeader,
|
|
274
|
+
note: detailsSuccess
|
|
275
|
+
? "Latest build report pointer (See subcollection for details)."
|
|
276
|
+
: "Latest build report pointer (WARNING: Partial detail records due to write error)."
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
await db.collection('computation_build_records').doc('latest').set(latestMetadata);
|
|
281
|
+
logger.log('SUCCESS', `[BuildReporter] Report ${buildId} saved. Re-runs: ${totalReRun}, New: ${totalRun}. Pointer Updated.`);
|
|
282
|
+
} catch (pointerErr) {
|
|
283
|
+
logger.log('FATAL', `[BuildReporter] Failed to update 'latest' pointer!`, pointerErr);
|
|
284
|
+
}
|
|
256
285
|
|
|
257
286
|
return {
|
|
258
287
|
success: true,
|