bulltrackers-module 1.0.653 → 1.0.654

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.
@@ -5,16 +5,14 @@
5
5
  * UPDATED: Enforces Strict One-Shot Policy (Standard -> HighMem -> Dead Letter).
6
6
  * UPDATED: Generates Google Cloud Trace Context (traceId/spanId) for end-to-end monitoring.
7
7
  * UPDATED: Added Schedule Awareness (Daily, Weekly, Monthly) to filter tasks by date.
8
- * UPDATED: Force Run now validates Root Data Availability before dispatching.
9
8
  */
10
9
 
11
10
  const { getExpectedDateStrings, getEarliestDataDates, normalizeName, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils.js');
12
11
  const { groupByPass, analyzeDateExecution } = require('../WorkflowOrchestrator.js');
13
12
  const { PubSubUtils } = require('../../core/utils/pubsub_utils');
14
13
  const { fetchComputationStatus } = require('../persistence/StatusRepository');
15
- // [UPDATED] Imported getAvailabilityWindow and checkRootDependencies
16
- const { checkRootDataAvailability, getAvailabilityWindow, checkRootDependencies } = require('../data/AvailabilityChecker');
17
- const { runFinalSweepCheck } = require('../tools/FinalSweepReporter');
14
+ const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
15
+ const { runFinalSweepCheck } = require('../tools/FinalSweepReporter'); // [NEW]
18
16
  const crypto = require('crypto');
19
17
 
20
18
  const OOM_THRESHOLD_MB = 1500; // Unused
@@ -232,71 +230,92 @@ async function handleForceRun(config, dependencies, computationManifest, reqBody
232
230
  }
233
231
 
234
232
  // 2. Determine Target Dates
235
- let targetDates = [];
233
+ let candidateDates = [];
236
234
  if (dateInput) {
237
235
  // Single Date Mode
238
- // We still perform the check, but for a single item array
239
- if (Array.isArray(dateInput)) {
240
- targetDates = dateInput;
241
- } else {
242
- targetDates = [dateInput];
243
- }
236
+ candidateDates = [dateInput];
244
237
  } else {
245
238
  // All Dates Mode (Backfill)
246
239
  logger.log('INFO', `[ForceRun] No date provided. Calculating date range for ${computationName}...`);
247
240
  const earliestDates = await getEarliestDataDates(config, dependencies);
248
241
  // Calculate from system start until today
249
- targetDates = getExpectedDateStrings(earliestDates.absoluteEarliest, new Date());
242
+ candidateDates = getExpectedDateStrings(earliestDates.absoluteEarliest, new Date());
250
243
  }
251
244
 
252
- // [NEW] 3. Filter Impossible Dates (Availability Check)
253
- logger.log('INFO', `[ForceRun] 🔍 Validating data availability for ${targetDates.length} candidate dates...`);
254
-
255
- // Sort dates to get efficient min/max for range query
256
- targetDates.sort();
257
- const startDate = targetDates[0];
258
- const endDate = targetDates[targetDates.length - 1];
245
+ logger.log('INFO', `[ForceRun] Checking ${candidateDates.length} candidate dates for runnability...`);
259
246
 
260
- const availabilityMap = await getAvailabilityWindow(dependencies, startDate, endDate);
261
- const validDates = [];
262
- const skippedStats = {};
263
-
264
- for (const date of targetDates) {
265
- const status = availabilityMap.get(date);
247
+ // 3. Filter to only runnable dates using analyzeDateExecution
248
+ const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
249
+ const calcsInPass = groupByPass(computationManifest, manifestItem.pass || "1");
250
+ const targetComputationNormalized = normalizeName(computationName);
251
+
252
+ // Filter to only the target computation
253
+ const targetCalcs = calcsInPass.filter(c => normalizeName(c.name) === targetComputationNormalized);
254
+
255
+ if (targetCalcs.length === 0) {
256
+ throw new Error(`Computation '${computationName}' not found in pass ${manifestItem.pass || "1"}`);
257
+ }
258
+
259
+ const runnableDates = [];
260
+ const skippedDates = [];
261
+
262
+ for (const dateStr of candidateDates) {
263
+ // Check root data availability
264
+ const rootDataStatus = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
265
+
266
+ // Get computation status for this date
267
+ const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
266
268
 
267
- // If no index exists, we pass an empty object (AvailabilityChecker will fail all checks)
268
- // This effectively filters out dates where we have NO knowledge of data
269
- const effectiveStatus = status || {};
269
+ // Check previous day status if needed
270
+ let prevDailyStatus = null;
271
+ if (targetCalcs.some(c => c.isHistorical)) {
272
+ const prevDate = new Date(dateStr + 'T00:00:00Z');
273
+ prevDate.setUTCDate(prevDate.getUTCDate() - 1);
274
+ prevDailyStatus = await fetchComputationStatus(prevDate.toISOString().slice(0, 10), config, dependencies);
275
+ }
276
+
277
+ // Analyze if this computation can run on this date
278
+ const report = analyzeDateExecution(dateStr, targetCalcs, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus);
270
279
 
271
- const check = checkRootDependencies(manifestItem, effectiveStatus);
280
+ // Check if the target computation is runnable, needs re-run, or has failed dependencies
281
+ const isRunnable = report.runnable.some(t => normalizeName(t.name) === targetComputationNormalized);
282
+ const needsReRun = report.reRuns.some(t => normalizeName(t.name) === targetComputationNormalized);
283
+ const hasFailedDep = report.failedDependency.some(t => normalizeName(t.name) === targetComputationNormalized);
284
+ const isImpossible = report.impossible.some(t => normalizeName(t.name) === targetComputationNormalized);
285
+ const isBlocked = report.blocked.some(t => normalizeName(t.name) === targetComputationNormalized);
272
286
 
273
- if (check.canRun) {
274
- validDates.push(date);
287
+ if (isRunnable || needsReRun || hasFailedDep) {
288
+ runnableDates.push(dateStr);
289
+ } else if (isImpossible) {
290
+ skippedDates.push({ date: dateStr, reason: report.impossible.find(t => normalizeName(t.name) === targetComputationNormalized)?.reason || 'Impossible' });
291
+ } else if (isBlocked) {
292
+ skippedDates.push({ date: dateStr, reason: report.blocked.find(t => normalizeName(t.name) === targetComputationNormalized)?.reason || 'Blocked' });
275
293
  } else {
276
- const reason = check.missing.length > 0 ? `Missing: ${check.missing.join(', ')}` : 'Dependencies not met';
277
- skippedStats[reason] = (skippedStats[reason] || 0) + 1;
294
+ skippedDates.push({ date: dateStr, reason: 'Not runnable (unknown reason)' });
278
295
  }
279
296
  }
280
-
281
- if (validDates.length === 0) {
282
- logger.log('WARN', `[ForceRun] 🛑 ABORTING: No valid dates found for ${computationName} out of ${targetDates.length} requested.`);
297
+
298
+ logger.log('INFO', `[ForceRun] ✅ Found ${runnableDates.length} runnable dates out of ${candidateDates.length} candidates`);
299
+ if (skippedDates.length > 0) {
300
+ logger.log('INFO', `[ForceRun] ⏭️ Skipped ${skippedDates.length} dates: ${skippedDates.slice(0, 5).map(s => `${s.date} (${s.reason})`).join(', ')}${skippedDates.length > 5 ? '...' : ''}`);
301
+ }
302
+
303
+ if (runnableDates.length === 0) {
283
304
  return {
284
- status: 'ABORTED',
305
+ status: 'NO_RUNNABLE_DATES',
285
306
  computation: computationName,
286
- reason: 'NO_DATA_AVAILABLE',
287
- skippedDetails: skippedStats
307
+ mode: dateInput ? 'SINGLE_DATE' : 'ALL_DATES',
308
+ datesChecked: candidateDates.length,
309
+ datesRunnable: 0,
310
+ skippedReasons: skippedDates.slice(0, 10)
288
311
  };
289
312
  }
313
+
314
+ logger.log('WARN', `[ForceRun] 🚨 MANUALLY Triggering ${computationName} for ${runnableDates.length} runnable dates. Pass: ${manifestItem.pass}`);
290
315
 
291
- if (validDates.length < targetDates.length) {
292
- logger.log('INFO', `[ForceRun] ⚠️ Filtered impossible dates: ${targetDates.length} requested -> ${validDates.length} valid.`, { skippedStats });
293
- }
294
-
295
- logger.log('WARN', `[ForceRun] 🚨 MANUALLY Triggering ${computationName} for ${validDates.length} VALID dates. Pass: ${manifestItem.pass}`);
296
-
297
- // 4. Construct Tasks
316
+ // 4. Construct Tasks (only for runnable dates)
298
317
  const dispatchId = crypto.randomUUID();
299
- const tasks = validDates.map(date => {
318
+ const tasks = runnableDates.map(date => {
300
319
  const traceId = crypto.randomBytes(16).toString('hex');
301
320
  const spanId = crypto.randomBytes(8).toString('hex');
302
321
  return {
@@ -312,7 +331,7 @@ async function handleForceRun(config, dependencies, computationManifest, reqBody
312
331
  };
313
332
  });
314
333
 
315
- // 5. Batch Publish (Chunked to stay under Pub/Sub limits)
334
+ // 4. Batch Publish (Chunked to stay under Pub/Sub limits)
316
335
  const CHUNK_SIZE = 250; // Safe batch size
317
336
  const topic = (reqBody.resources === 'high-mem')
318
337
  ? (config.computationTopicHighMem || 'computation-tasks-highmem')
@@ -337,10 +356,11 @@ async function handleForceRun(config, dependencies, computationManifest, reqBody
337
356
  return {
338
357
  status: 'FORCED',
339
358
  computation: computationName,
340
- mode: dateInput ? 'SINGLE/ARRAY_DATE' : 'ALL_DATES',
341
- datesRequested: targetDates.length,
359
+ mode: dateInput ? 'SINGLE_DATE' : 'ALL_DATES',
360
+ datesChecked: candidateDates.length,
361
+ datesRunnable: runnableDates.length,
342
362
  datesTriggered: dispatchedCount,
343
- skippedImpossible: targetDates.length - dispatchedCount,
363
+ skippedCount: skippedDates.length,
344
364
  targetTopic: topic
345
365
  };
346
366
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.653",
3
+ "version": "1.0.654",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [