bulltrackers-module 1.0.342 → 1.0.344

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.
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * FILENAME: computation-system/helpers/computation_dispatcher.js
3
3
  * PURPOSE: Sequential Cursor-Based Dispatcher.
4
+ * UPDATED: Implemented "Fast-Forward" Scanning Loop to skip empty dates efficiently.
4
5
  * UPDATED: Enforces Strict One-Shot Policy (Standard -> HighMem -> Dead Letter).
5
- * UPDATED: Prevents infinite loops by permanently ignoring deterministic failures.
6
6
  * UPDATED: Generates Google Cloud Trace Context (traceId/spanId) for end-to-end monitoring.
7
7
  */
8
8
 
@@ -12,9 +12,9 @@ const { PubSubUtils } = require('../../core/utils/pubsub_utils');
12
12
  const { fetchComputationStatus } = require('../persistence/StatusRepository');
13
13
  const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
14
14
  const crypto = require('crypto');
15
- const monConfig = require('../config/monitoring_config'); // [NEW]
15
+ const monConfig = require('../config/monitoring_config');
16
16
 
17
- const OOM_THRESHOLD_MB = 1500;
17
+ const OOM_THRESHOLD_MB = 1500; // Unused
18
18
  const BASE_SECONDS_PER_WEIGHT_UNIT = 3;
19
19
  const SESSION_CACHE_DURATION_MS = 1000 * 60 * 30; // 30 Minutes
20
20
  const STALE_LOCK_THRESHOLD_MS = 1000 * 60 * 15;
@@ -281,7 +281,6 @@ async function handleSweepDispatch(config, dependencies, computationManifest, re
281
281
  const currentDispatchId = crypto.randomUUID();
282
282
 
283
283
  const tasksPayload = validTasks.map(t => {
284
- // [NEW] Generate Eyeball Trace
285
284
  const traceId = crypto.randomBytes(16).toString('hex');
286
285
  const spanId = crypto.randomBytes(8).toString('hex');
287
286
 
@@ -294,7 +293,6 @@ async function handleSweepDispatch(config, dependencies, computationManifest, re
294
293
  dispatchId: currentDispatchId,
295
294
  triggerReason: 'SWEEP_RECOVERY',
296
295
  resources: 'high-mem', // FORCE
297
- // [NEW] Attach the eyeball
298
296
  traceContext: {
299
297
  traceId: traceId,
300
298
  spanId: spanId,
@@ -315,7 +313,7 @@ async function handleSweepDispatch(config, dependencies, computationManifest, re
315
313
  }
316
314
 
317
315
  // =============================================================================
318
- // LOGIC: Standard Dispatch (Original)
316
+ // LOGIC: Standard Dispatch (Fast-Forward Enabled)
319
317
  // =============================================================================
320
318
  async function handleStandardDispatch(config, dependencies, computationManifest, reqBody) {
321
319
  const { logger, db } = dependencies;
@@ -336,16 +334,29 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
336
334
  const sessionDates = await getStableDateSession(config, dependencies, passToRun, dateLimitStr, forceRebuild);
337
335
  if (!sessionDates || sessionDates.length === 0) return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
338
336
 
339
- let selectedDate = null;
337
+ // --- Fast-Forward Loop Configuration ---
338
+ // Scans up to 50 dates or 40 seconds to find work, avoiding empty "wait loops"
339
+ const MAX_SCAN_DEPTH = 50;
340
+ const TIME_LIMIT_MS = 40000;
341
+ const startTime = Date.now();
342
+
343
+ let currentCursor = targetCursorN;
340
344
  let selectedTasks = [];
345
+ let selectedDate = null;
346
+ let datesScanned = 0;
341
347
 
342
- if (targetCursorN <= sessionDates.length) {
343
- selectedDate = sessionDates[targetCursorN - 1];
344
- } else {
345
- return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
346
- }
348
+ // Loop until work is found, end is reached, or safety limits hit
349
+ while (currentCursor <= sessionDates.length) {
350
+ datesScanned++;
351
+ selectedDate = sessionDates[currentCursor - 1]; // 0-indexed array
352
+
353
+ // 1. Safety Break (Prevent Timeout)
354
+ if ((Date.now() - startTime) > TIME_LIMIT_MS || datesScanned > MAX_SCAN_DEPTH) {
355
+ logger.log('INFO', `[Dispatcher] ⏩ Fast-forward paused at ${selectedDate} after scanning ${datesScanned} dates.`);
356
+ break;
357
+ }
347
358
 
348
- if (selectedDate) {
359
+ // 2. Analyze Date
349
360
  const earliestDates = await getEarliestDataDates(config, dependencies);
350
361
  let prevDailyStatusPromise = Promise.resolve(null);
351
362
  if (calcsInThisPass.some(c => c.isHistorical)) {
@@ -362,48 +373,70 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
362
373
  checkRootDataAvailability(selectedDate, config, dependencies, DEFINITIVE_EARLIEST_DATES)
363
374
  ]);
364
375
 
365
- if (availability && availability.status) { // Just check availability exists
376
+ if (availability && availability.status) {
366
377
  const report = analyzeDateExecution(selectedDate, calcsInThisPass, availability.status, dailyStatus, manifestMap, prevDailyStatus);
367
378
  let rawTasks = [...report.runnable, ...report.reRuns];
368
379
 
369
380
  if (rawTasks.length > 0) {
370
381
  rawTasks = await attemptSimHashResolution(dependencies, selectedDate, rawTasks, dailyStatus, manifestMap);
371
- selectedTasks = await filterActiveTasks(db, selectedDate, passToRun, rawTasks, logger);
372
- }
373
-
374
- if (selectedTasks.length > 0) {
375
- // Split Logic: Moves OOMs to High-Mem, drops dead letters
376
- const { standard, highMem } = await splitRoutes(db, selectedDate, passToRun, selectedTasks, logger);
377
- selectedTasks = [...standard, ...highMem];
382
+ const activeTasks = await filterActiveTasks(db, selectedDate, passToRun, rawTasks, logger);
383
+
384
+ if (activeTasks.length > 0) {
385
+ const { standard, highMem } = await splitRoutes(db, selectedDate, passToRun, activeTasks, logger);
386
+ selectedTasks = [...standard, ...highMem];
387
+
388
+ if (selectedTasks.length > 0) {
389
+ // Found work! Break loop to dispatch.
390
+ break;
391
+ }
392
+ }
378
393
  }
379
394
  }
395
+
396
+ // No work found for this date. Fast-forward to next.
397
+ currentCursor++;
380
398
  }
381
399
 
400
+ // --- Result Handling ---
401
+
402
+ // Case 1: Satiated (Scanned to end of session with no work)
403
+ if (currentCursor > sessionDates.length && selectedTasks.length === 0) {
404
+ return {
405
+ status: 'CONTINUE_PASS',
406
+ dateProcessed: selectedDate,
407
+ dispatched: 0,
408
+ n_cursor_ignored: false,
409
+ remainingDates: 0,
410
+ nextCursor: currentCursor // Matches length + 1
411
+ };
412
+ }
413
+
414
+ // Case 2: Paused by Limit (No work found yet, but more dates remain)
382
415
  if (selectedTasks.length === 0) {
383
416
  return {
384
417
  status: 'CONTINUE_PASS',
385
418
  dateProcessed: selectedDate,
386
419
  dispatched: 0,
387
420
  n_cursor_ignored: false,
388
- etaSeconds: 0,
389
- remainingDates: sessionDates.length - targetCursorN
421
+ remainingDates: sessionDates.length - currentCursor + 1,
422
+ nextCursor: currentCursor // Resume from here
390
423
  };
391
424
  }
392
425
 
426
+ // Case 3: Work Found (Dispatching)
393
427
  const totalweight = selectedTasks.reduce((sum, t) => sum + (manifestWeightMap.get(normalizeName(t.name)) || 1.0), 0);
394
428
  const currentDispatchId = crypto.randomUUID();
395
429
  const etaSeconds = Math.max(20, Math.ceil(totalweight * BASE_SECONDS_PER_WEIGHT_UNIT));
396
430
 
397
- const taskDetails = selectedTasks.map(t => `${t.name} (${t.reason})`);
398
- logger.log('INFO', `[Dispatcher] Dispatching ${selectedTasks.length} tasks for ${selectedDate}.`, {
399
- date: selectedDate, pass: passToRun, dispatchedCount: selectedTasks.length, etaSeconds, dispatchId: currentDispatchId, details: taskDetails
400
- });
431
+ if (datesScanned > 1) {
432
+ logger.log('INFO', `[Dispatcher] Fast-forwarded ${datesScanned - 1} empty dates. Dispatching ${selectedTasks.length} tasks for ${selectedDate}.`);
433
+ } else {
434
+ logger.log('INFO', `[Dispatcher] ✅ Dispatching ${selectedTasks.length} tasks for ${selectedDate}.`);
435
+ }
401
436
 
402
437
  const mapToTaskPayload = (t) => {
403
- // [NEW] Generate Eyeball Trace
404
438
  const traceId = crypto.randomBytes(16).toString('hex');
405
439
  const spanId = crypto.randomBytes(8).toString('hex');
406
-
407
440
  return {
408
441
  ...t,
409
442
  action: 'RUN_COMPUTATION_DATE',
@@ -413,7 +446,6 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
413
446
  dispatchId: currentDispatchId,
414
447
  triggerReason: t.reason,
415
448
  resources: t.resources || 'standard',
416
- // [NEW] Attach the eyeball
417
449
  traceContext: {
418
450
  traceId: traceId,
419
451
  spanId: spanId,
@@ -448,13 +480,15 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
448
480
  dispatched: selectedTasks.length,
449
481
  n_cursor_ignored: false,
450
482
  etaSeconds: etaSeconds,
451
- remainingDates: sessionDates.length - targetCursorN
483
+ remainingDates: sessionDates.length - targetCursorN,
484
+ nextCursor: currentCursor + 1 // Start next scan AFTER this date
452
485
  };
453
486
  }
454
487
 
455
488
  // =============================================================================
456
489
  // HELPER: Route Splitting (One-Shot Enforcement)
457
490
  // =============================================================================
491
+ // [UPDATED] Route Splitting with Version-Aware Dead Lettering
458
492
  async function splitRoutes(db, date, pass, tasks, logger) {
459
493
  const standard = [];
460
494
  const highMem = [];
@@ -465,32 +499,51 @@ async function splitRoutes(db, date, pass, tasks, logger) {
465
499
  const doc = await db.doc(ledgerPath).get();
466
500
 
467
501
  if (!doc.exists) {
468
- // New task -> Standard
469
502
  standard.push(task);
470
503
  continue;
471
504
  }
472
505
 
473
506
  const data = doc.data();
474
507
 
475
- // If it FAILED, we check if we can escalate it.
508
+ // CHECK: Is the task currently running or pending?
509
+ if (['PENDING', 'IN_PROGRESS'].includes(data.status)) {
510
+ // It's active, don't dispatch again
511
+ continue;
512
+ }
513
+
476
514
  if (data.status === 'FAILED') {
477
515
  const stage = data.error?.stage;
478
516
 
479
- // 1. QUALITY / LOGIC FAIL: Dead Letter (Drop it)
480
- if (['QUALITY_CIRCUIT_BREAKER', 'SEMANTIC_GATE'].includes(stage)) {
481
- logger.log('WARN', `[Dispatcher] 🛑 Dropping ${name} - Deterministic Failure (${stage}).`);
482
- continue;
517
+ // 1. DETERMINISTIC FAILURES (Never Retry)
518
+ if (['QUALITY_CIRCUIT_BREAKER', 'SEMANTIC_GATE', 'SHARDING_LIMIT_EXCEEDED'].includes(stage)) {
519
+ logger.log('WARN', `[Dispatcher] 🛑 Dropping ${name} - Deterministic Failure (${stage}).`);
520
+ continue;
483
521
  }
484
522
 
485
- // 2. PREVIOUSLY HIGH MEM FAIL: Dead Letter (Drop it)
523
+ // 2. HIGH MEMORY FAILURE HANDLING (The New Logic)
486
524
  if (data.resourceTier === 'high-mem') {
487
- logger.log('WARN', `[Dispatcher] 🛑 Dropping ${name} - Failed on High-Mem already.`);
488
- continue;
525
+ const failedHash = data.hash || data.composition?.code; // Support legacy or new structure
526
+ const currentHash = task.hash;
527
+
528
+ // A. EXACT CODE MATCH: It failed High-Mem with THIS code.
529
+ if (failedHash === currentHash) {
530
+ logger.log('WARN', `[Dispatcher] 💀 Dead End: ${name} failed High-Mem on this version (${currentHash.slice(0,6)}). Waiting for code fix.`);
531
+ continue; // STOP. Do not retry.
532
+ }
533
+
534
+ // B. CODE MISMATCH: The code has changed since the High-Mem failure.
535
+ // We reset it to 'standard' to see if the fix optimized memory usage.
536
+ else {
537
+ logger.log('INFO', `[Dispatcher] 🔄 Code Updated for ${name}. Resetting High-Mem failure to Standard retry.`);
538
+ standard.push({
539
+ ...task,
540
+ reason: 'Retry: Code Version Changed'
541
+ });
542
+ continue;
543
+ }
489
544
  }
490
545
 
491
- // 3. STANDARD FAIL (Crash/OOM): Promote to High Mem (Retry)
492
- // If it failed standard, we give it ONE shot on high-mem.
493
- // Note: Even if it was an "Unknown" error, we promote to High-Mem to cover OOMs that looked like crashes.
546
+ // 3. STANDARD FAILURE -> PROMOTE TO HIGH MEM
494
547
  highMem.push({
495
548
  ...task,
496
549
  resources: 'high-mem',
@@ -498,7 +551,8 @@ async function splitRoutes(db, date, pass, tasks, logger) {
498
551
  });
499
552
 
500
553
  } else {
501
- // If status is not FAILED (e.g. was Zombie and filterActiveTasks passed it), retry Standard.
554
+ // Status is likely COMPLETED (but filterActiveTasks should have caught this)
555
+ // or some other state. Default to standard if we are here.
502
556
  standard.push(task);
503
557
  }
504
558
  }
@@ -2,6 +2,7 @@
2
2
  * FILENAME: computation-system/helpers/computation_worker.js
3
3
  * UPDATED: Implements Google Cloud Monitoring Heartbeats.
4
4
  * UPDATED: Implements Structured Logging with Trace Context.
5
+ * FIXED: 'manifest' scope issue (declared outside try/catch).
5
6
  */
6
7
 
7
8
  const { executeDispatchTask } = require('../WorkflowOrchestrator.js');
@@ -143,8 +144,6 @@ async function handleComputationTask(message, config, dependencies) {
143
144
  const workerId = process.env.K_REVISION || os.hostname();
144
145
 
145
146
  // 2. Initialize Trace-Aware Logger (The "Eyeball")
146
- // If traceContext is present (from dispatcher), we inject it into globalMetadata
147
- // so ALL logs from this execution line up in Google Trace.
148
147
  const globalMetadata = {};
149
148
  if (traceContext && monConfig.enabled) {
150
149
  globalMetadata['logging.googleapis.com/trace'] = `projects/${monConfig.project}/traces/${traceContext.traceId}`;
@@ -155,7 +154,7 @@ async function handleComputationTask(message, config, dependencies) {
155
154
  const logger = new StructuredLogger({
156
155
  minLevel: config.minLevel || 'INFO',
157
156
  enableStructured: true,
158
- globalMetadata, // [NEW] Inject Trace
157
+ globalMetadata,
159
158
  ...config
160
159
  });
161
160
 
@@ -172,11 +171,13 @@ async function handleComputationTask(message, config, dependencies) {
172
171
  logger.log('INFO', `[Worker] 📥 Task: ${computation} (${date}) [Tier: ${resourceTier}] [ID: ${dispatchId}]`);
173
172
 
174
173
  // --- STEP 2: START DUAL HEARTBEATS ---
175
- // Starts both Firestore writes (legacy) AND Google Metrics (new)
176
174
  const heartbeats = startMemoryHeartbeat(db, ledgerPath, workerId, computation, traceContext?.traceId);
177
175
 
176
+ // [FIX] Declare manifest here so it is accessible in both try and catch blocks
177
+ let manifest;
178
+
178
179
  try {
179
- const manifest = getManifest(config.activeProductLines || [], calculations, runDeps);
180
+ manifest = getManifest(config.activeProductLines || [], calculations, runDeps);
180
181
  const startTime = Date.now();
181
182
 
182
183
  const result = await executeDispatchTask(
@@ -207,7 +208,6 @@ async function handleComputationTask(message, config, dependencies) {
207
208
  };
208
209
 
209
210
  await db.doc(ledgerPath).update({ status: 'COMPLETED', completedAt: new Date() });
210
- // Use the new recorder which also prints JSON for logs
211
211
  await recordRunAttempt(db, { date, computation, pass }, 'SUCCESS', null, metrics, triggerReason, resourceTier);
212
212
 
213
213
  } catch (err) {
@@ -215,10 +215,25 @@ async function handleComputationTask(message, config, dependencies) {
215
215
 
216
216
  const isDeterministic = ['SHARDING_LIMIT_EXCEEDED', 'QUALITY_CIRCUIT_BREAKER', 'SEMANTIC_GATE'].includes(err.stage);
217
217
 
218
+ // [FIX] Now we can safely access 'manifest' to find the current hash
219
+ let currentHash = 'unknown';
220
+ if (typeof manifest !== 'undefined' && Array.isArray(manifest)) {
221
+ const item = manifest.find(c => normalizeName(c.name) === normalizeName(computation));
222
+ if (item) currentHash = item.hash;
223
+ }
224
+
218
225
  if (isDeterministic || (message.deliveryAttempt || 1) >= MAX_RETRIES) {
219
226
  const errorPayload = { message: err.message, stage: err.stage || 'FATAL' };
220
227
 
221
- await db.doc(ledgerPath).set({ status: 'FAILED', error: errorPayload, failedAt: new Date() }, { merge: true });
228
+ // [UPDATED] We now save 'hash' and 'resourceTier' to the ledger on failure
229
+ await db.doc(ledgerPath).set({
230
+ status: 'FAILED',
231
+ error: errorPayload,
232
+ failedAt: new Date(),
233
+ hash: currentHash, // <--- CRITICAL FOR DISPATCHER DEAD-LETTER CHECK
234
+ resourceTier: resourceTier
235
+ }, { merge: true });
236
+
222
237
  await recordRunAttempt(db, { date, computation, pass }, 'FAILURE', { message: err.message, stage: err.stage || 'FATAL' }, { peakMemoryMB: heartbeats.getPeak() }, triggerReason, resourceTier);
223
238
  return;
224
239
  }
@@ -1,6 +1,6 @@
1
1
  # Cloud Workflows: Precision Cursor-Based Orchestrator
2
- # SIMPLE MODE: Dispatch -> Wait ETA -> Next Date
3
- # UPGRADED: Added "Sweep & Verify" Step for High-Mem Recovery
2
+ # FAST-FORWARD MODE: Dispatch -> (Internal Loop) -> Wait ETA -> Jump to Next Valid Cursor
3
+ # UPGRADED: Supports 'nextCursor' to skip empty date ranges instantly.
4
4
 
5
5
  main:
6
6
  params: [input]
@@ -39,12 +39,12 @@ main:
39
39
 
40
40
  - evaluate_dispatch:
41
41
  switch:
42
- # 1. End of Session (Dispatcher reached end of date list)
42
+ # 1. End of Session (Dispatcher reached absolute end of date list)
43
43
  - condition: '${dispatch_res.body.status == "MOVE_TO_NEXT_PASS"}'
44
44
  assign:
45
45
  - pass_complete: true
46
46
 
47
- # 2. Satiation Check (Specific to date/logic)
47
+ # 2. Satiation Check (Dispatcher scanned to end, found nothing)
48
48
  - condition: '${dispatch_res.body.status == "CONTINUE_PASS" and dispatch_res.body.remainingDates == 0 and dispatch_res.body.dispatched == 0}'
49
49
  steps:
50
50
  - log_satiation:
@@ -55,7 +55,7 @@ main:
55
55
  assign:
56
56
  - pass_complete: true
57
57
 
58
- # 3. Work Dispatched: Wait ETA -> Move Next (Ignored flag is FALSE)
58
+ # 3. Work Dispatched: Wait ETA -> Jump to 'nextCursor'
59
59
  - condition: '${dispatch_res.body.dispatched > 0}'
60
60
  steps:
61
61
  - wait_for_completion:
@@ -64,25 +64,26 @@ main:
64
64
  seconds: '${int(dispatch_res.body.etaSeconds)}'
65
65
  - update_cursor:
66
66
  assign:
67
- - n_cursor: '${if(dispatch_res.body.n_cursor_ignored, n_cursor, n_cursor + 1)}'
67
+ # CRITICAL CHANGE: Use dispatcher's specific jump target
68
+ - n_cursor: '${default(dispatch_res.body.nextCursor, n_cursor + 1)}'
68
69
  - next_loop_work:
69
70
  next: sequential_date_loop
70
71
 
71
- # 4. No Work (Clean or Busy): Move Next Immediately
72
+ # 4. No Work (Fast-Forward limit reached or Single Check): Jump to 'nextCursor'
72
73
  - condition: '${dispatch_res.body.dispatched == 0}'
73
74
  steps:
74
75
  - wait_short:
75
76
  call: sys.sleep
76
77
  args:
77
- seconds: 2 # Tiny debounce
78
+ seconds: 2 # Tiny debounce for API limits
78
79
  - update_cursor_retry:
79
80
  assign:
80
- # Dispatcher sends n_cursor_ignored=false, so we increment.
81
- - n_cursor: '${if(dispatch_res.body.n_cursor_ignored, n_cursor, n_cursor + 1)}'
81
+ # CRITICAL CHANGE: Resume exactly where the Fast-Forward loop stopped
82
+ - n_cursor: '${default(dispatch_res.body.nextCursor, n_cursor + 1)}'
82
83
  - next_loop_retry:
83
84
  next: sequential_date_loop
84
85
 
85
- # --- NEW STEP: VERIFICATION & SWEEP ---
86
+ # --- VERIFICATION & SWEEP (No changes needed here) ---
86
87
  - verify_pass_completion:
87
88
  call: http.post
88
89
  args:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.342",
3
+ "version": "1.0.344",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [