bulltrackers-module 1.0.309 → 1.0.311

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,6 +1,7 @@
1
1
  /**
2
2
  * FILENAME: computation-system/helpers/computation_dispatcher.js
3
3
  * PURPOSE: Sequential Cursor-Based Dispatcher with Hyper-Verbose Telemetry.
4
+ * FIX: Added dispatchId generation and triggerReason mapping.
4
5
  */
5
6
 
6
7
  const { getExpectedDateStrings, getEarliestDataDates, normalizeName, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils.js');
@@ -8,6 +9,7 @@ const { groupByPass, analyzeDateExecution } = require('../WorkflowOrchestrator.j
8
9
  const { PubSubUtils } = require('../../core/utils/pubsub_utils');
9
10
  const { fetchComputationStatus } = require('../persistence/StatusRepository');
10
11
  const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
12
+ const crypto = require('crypto'); // [NEW] Required for dispatchId
11
13
 
12
14
  const OOM_THRESHOLD_MB = 1500;
13
15
  const SECONDS_PER_CALC_MARGIN = 25;
@@ -37,7 +39,6 @@ async function dispatchComputationPass(config, dependencies, computationManifest
37
39
  const { logger, db } = dependencies;
38
40
  const pubsubUtils = new PubSubUtils(dependencies);
39
41
 
40
- // 1. Capture Inputs
41
42
  const passToRun = String(reqBody.pass || config.COMPUTATION_PASS_TO_RUN || "1");
42
43
  const targetCursorN = parseInt(reqBody.cursorIndex || 1);
43
44
  const dateLimitStr = reqBody.date || config.date || "2025-01-01";
@@ -53,55 +54,34 @@ async function dispatchComputationPass(config, dependencies, computationManifest
53
54
  return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
54
55
  }
55
56
 
56
- // 2. Discover Discovery Boundaries
57
57
  const earliestDates = await getEarliestDataDates(config, dependencies);
58
- logger.log('INFO', `[Dispatcher] Discovery Boundaries: Earliest=${earliestDates.absoluteEarliest.toISOString().slice(0,10)}, Limit=${dateLimitStr}`);
59
-
60
58
  const allDates = getExpectedDateStrings(earliestDates.absoluteEarliest, new Date(dateLimitStr + 'T00:00:00Z'));
61
59
 
62
60
  if (allDates.length === 0) {
63
- logger.log('ERROR', `[Dispatcher] ❌ Date range is empty. Check if dateLimit is before earliest data.`);
61
+ logger.log('ERROR', `[Dispatcher] ❌ Date range is empty.`);
64
62
  return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
65
63
  }
66
64
 
67
- // 3. Date Scanning Loop
68
65
  const dirtyDates = [];
69
- let blockedCount = 0;
70
- let upToDateCount = 0;
71
-
72
- logger.log('INFO', `[Dispatcher] Scanning ${allDates.length} dates for work...`);
73
-
74
66
  for (const d of allDates) {
75
67
  const dailyStatus = await fetchComputationStatus(d, config, dependencies);
76
68
  const availability = await checkRootDataAvailability(d, config, dependencies, DEFINITIVE_EARLIEST_DATES);
77
69
 
78
- // Detailed check on availability status
79
- if (!availability || !availability.status.hasPrices) {
80
- // Log every 30 days to avoid log spam if data is missing for long periods
81
- if (allDates.indexOf(d) % 30 === 0) logger.log('DEBUG', `[Dispatcher] ${d}: Root Data Index Missing or Price=false.`);
82
- blockedCount++;
83
- continue;
84
- }
70
+ if (!availability || !availability.status.hasPrices) continue;
85
71
 
86
72
  const report = analyzeDateExecution(d, calcsInThisPass, availability.status, dailyStatus, manifestMap, null);
87
73
  const tasks = [...report.runnable, ...report.reRuns];
88
74
 
89
75
  if (tasks.length > 0) {
90
- logger.log('INFO', `[Dispatcher] ✨ Found Dirty Date: ${d} (${tasks.length} tasks)`);
91
76
  dirtyDates.push({ date: d, tasks });
92
- } else {
93
- upToDateCount++;
94
77
  }
95
78
  }
96
79
 
97
- logger.log('INFO', `[Dispatcher] Scan Complete: ${dirtyDates.length} dirty, ${upToDateCount} up-to-date, ${blockedCount} blocked/missing data.`);
98
-
99
80
  let selectedDate = null;
100
81
  let selectedTasks = [];
101
82
  let isReroute = false;
102
83
  let isSweep = false;
103
84
 
104
- // 4. Cursor Logic
105
85
  if (targetCursorN > 1 && (targetCursorN - 2) < dirtyDates.length) {
106
86
  const prevEntry = dirtyDates[targetCursorN - 2];
107
87
  const reroutes = await getHighMemReroutes(db, prevEntry.date, passToRun, prevEntry.tasks);
@@ -110,7 +90,6 @@ async function dispatchComputationPass(config, dependencies, computationManifest
110
90
  selectedDate = prevEntry.date;
111
91
  selectedTasks = reroutes;
112
92
  isReroute = true;
113
- logger.log('INFO', `[Dispatcher] 🔄 Reroute detected for ${selectedDate}. Retrying same cursor position with High-Mem.`);
114
93
  }
115
94
  }
116
95
 
@@ -119,26 +98,32 @@ async function dispatchComputationPass(config, dependencies, computationManifest
119
98
  const entry = dirtyDates[targetCursorN - 1];
120
99
  selectedDate = entry.date;
121
100
  selectedTasks = entry.tasks;
122
- logger.log('INFO', `[Dispatcher] Selecting Dirty Date #${targetCursorN}: ${selectedDate}`);
123
- } else {
124
- if (dirtyDates.length > 0) {
125
- isSweep = true;
126
- selectedDate = dirtyDates[0].date;
127
- selectedTasks = dirtyDates[0].tasks;
128
- logger.log('INFO', `[Dispatcher] 🧹 Satiation Sweep: Checking earliest dirty date ${selectedDate}`);
129
- }
101
+ } else if (dirtyDates.length > 0) {
102
+ isSweep = true;
103
+ selectedDate = dirtyDates[0].date;
104
+ selectedTasks = dirtyDates[0].tasks;
130
105
  }
131
106
  }
132
107
 
133
- // 5. Termination Check
134
108
  if (!selectedDate) {
135
- logger.log('SUCCESS', `[Dispatcher] ✅ Pass ${passToRun} is fully satiated. Signalling MOVE_TO_NEXT_PASS.`);
136
109
  return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0, etaSeconds: 0 };
137
110
  }
138
111
 
139
- // 6. Pub/Sub Dispatch
140
- const standardTasks = selectedTasks.filter(t => t.resources !== 'high-mem').map(t => ({ ...t, action: 'RUN_COMPUTATION_DATE', computation: t.name, date: selectedDate, pass: passToRun }));
141
- const highMemTasks = selectedTasks.filter(t => t.resources === 'high-mem').map(t => ({ ...t, action: 'RUN_COMPUTATION_DATE', computation: t.name, date: selectedDate, pass: passToRun }));
112
+ // [FIX] Generate unique Dispatch ID for this wave and map triggerReason
113
+ const currentDispatchId = crypto.randomUUID();
114
+
115
+ const mapToTaskPayload = (t) => ({
116
+ ...t,
117
+ action: 'RUN_COMPUTATION_DATE',
118
+ computation: t.name,
119
+ date: selectedDate,
120
+ pass: passToRun,
121
+ dispatchId: currentDispatchId, // [NEW] Added for Worker Audit Lease
122
+ triggerReason: t.reason // [NEW] Map 'reason' from Orchestrator to 'triggerReason'
123
+ });
124
+
125
+ const standardTasks = selectedTasks.filter(t => t.resources !== 'high-mem').map(mapToTaskPayload);
126
+ const highMemTasks = selectedTasks.filter(t => t.resources === 'high-mem').map(mapToTaskPayload);
142
127
 
143
128
  const pubPromises = [];
144
129
  if (standardTasks.length > 0) {
@@ -159,8 +144,6 @@ async function dispatchComputationPass(config, dependencies, computationManifest
159
144
 
160
145
  const etaSeconds = Math.max(20, selectedTasks.length * SECONDS_PER_CALC_MARGIN);
161
146
 
162
- logger.log('INFO', `[Dispatcher] 🛰️ DISPATCHED ${selectedTasks.length} tasks for ${selectedDate}. ETA ${etaSeconds}s.`);
163
-
164
147
  return {
165
148
  status : isSweep ? 'RECOVERY' : 'CONTINUE_PASS',
166
149
  dateProcessed : selectedDate,
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * FILENAME: computation-system/helpers/computation_worker.js
3
- * UPDATED: Removed redundant Callback and Sentinel logic.
4
- * The system now relies on Dispatcher cursor satiation.
3
+ * UPDATED: Fixed Firestore 'undefined' field error for dispatchId.
5
4
  */
6
5
 
7
6
  const { executeDispatchTask } = require('../WorkflowOrchestrator.js');
@@ -17,7 +16,6 @@ const calculations = calculationPackage.calculations;
17
16
 
18
17
  const MAX_RETRIES = 3;
19
18
 
20
- /** Black Box Recorder for Peak Memory. */
21
19
  function startMemoryHeartbeat(db, ledgerPath, intervalMs = 2000) {
22
20
  let peakRss = 0;
23
21
  const timer = setInterval(async () => {
@@ -48,13 +46,15 @@ async function handleComputationTask(message, config, dependencies) {
48
46
 
49
47
  logger.log('INFO', `[Worker] 📥 Task: ${computation} (${date}) [Tier: ${resourceTier}]`);
50
48
 
51
- // 1. Audit Lease (Lease on life for the monitor to see)
52
- await db.doc(ledgerPath).set({
49
+ // [FIX] Build document object and only add dispatchId if it is defined
50
+ const leaseData = {
53
51
  status: 'IN_PROGRESS',
54
52
  workerId: process.env.K_REVISION || os.hostname(),
55
- startedAt: new Date(),
56
- dispatchId
57
- }, { merge: true });
53
+ startedAt: new Date()
54
+ };
55
+ if (dispatchId) leaseData.dispatchId = dispatchId;
56
+
57
+ await db.doc(ledgerPath).set(leaseData, { merge: true });
58
58
 
59
59
  const heartbeat = startMemoryHeartbeat(db, ledgerPath);
60
60
 
@@ -84,7 +84,6 @@ async function handleComputationTask(message, config, dependencies) {
84
84
  composition: calcUpdate.composition
85
85
  };
86
86
 
87
- // Mark ledger as completed
88
87
  await db.doc(ledgerPath).update({ status: 'COMPLETED', completedAt: new Date() });
89
88
  await recordRunAttempt(db, { date, computation, pass }, 'SUCCESS', null, metrics, triggerReason, resourceTier);
90
89
 
@@ -95,9 +94,9 @@ async function handleComputationTask(message, config, dependencies) {
95
94
  if (isDeterministic || (message.deliveryAttempt || 1) >= MAX_RETRIES) {
96
95
  await db.doc(ledgerPath).set({ status: 'FAILED', error: err.message, failedAt: new Date() }, { merge: true });
97
96
  await recordRunAttempt(db, { date, computation, pass }, 'FAILURE', { message: err.message, stage: err.stage || 'FATAL' }, { peakMemoryMB: heartbeat.getPeak() }, triggerReason, resourceTier);
98
- return; // Exit without throwing to prevent endless Pub/Sub retries for dead logic
97
+ return;
99
98
  }
100
- throw err; // Trigger Pub/Sub retry for non-deterministic transient errors
99
+ throw err;
101
100
  }
102
101
  }
103
102
 
@@ -1,5 +1,6 @@
1
1
  # Cloud Workflows: Precision Cursor-Based Orchestrator
2
2
  # PURPOSE: Orchestrates 5 passes with dynamic date detection and cursor logic.
3
+ # UPDATED: Added Short-Circuit logic to break infinite loops on empty dispatches.
3
4
 
4
5
  main:
5
6
  params: [input]
@@ -26,6 +27,7 @@ main:
26
27
  assign:
27
28
  - n_cursor: 1
28
29
  - pass_complete: false
30
+ - consecutive_empty_dispatches: 0 # Track consecutive "duds" to prevent infinite loops
29
31
 
30
32
  - sequential_date_loop:
31
33
  switch:
@@ -49,9 +51,12 @@ main:
49
51
  assign:
50
52
  - pass_complete: true
51
53
 
52
- # State 2: Tasks were dispatched
54
+ # State 2: Tasks were dispatched (Healthy State)
53
55
  - condition: '${dispatch_res.body.dispatched > 0}'
54
56
  steps:
57
+ - reset_retry_counter:
58
+ assign:
59
+ - consecutive_empty_dispatches: 0 # Reset counter because progress was made
55
60
  - log_dispatch:
56
61
  call: sys.log
57
62
  args:
@@ -64,8 +69,45 @@ main:
64
69
  assign:
65
70
  # If n_cursor_ignored is true, stay on same N to retry (e.g. for high-mem)
66
71
  - n_cursor: '${if(dispatch_res.body.n_cursor_ignored, n_cursor, n_cursor + 1)}'
67
- - next_loop:
72
+ - next_loop_work:
68
73
  next: sequential_date_loop
69
74
 
75
+ # State 3: No tasks dispatched (Potential Infinite Loop Scenario)
76
+ # The Dispatcher is "Continuing" but found nothing runnable on the target date.
77
+ - condition: '${dispatch_res.body.dispatched == 0}'
78
+ steps:
79
+ - increment_retry:
80
+ assign:
81
+ - consecutive_empty_dispatches: '${consecutive_empty_dispatches + 1}'
82
+ - check_break_condition:
83
+ switch:
84
+ # If we have tried 3 times in a row with 0 results, assume the date is "stuck"
85
+ - condition: '${consecutive_empty_dispatches >= 3}'
86
+ steps:
87
+ - log_break:
88
+ call: sys.log
89
+ args:
90
+ text: '${"Pass " + pass_id + " - 🛑 FORCE BREAK: 3 consecutive empty dispatches. Moving to next pass to prevent infinite loop."}'
91
+ - force_complete:
92
+ assign:
93
+ - pass_complete: true
94
+ # Otherwise, wait briefly and retry (or move cursor depending on dispatcher logic)
95
+ - condition: '${true}'
96
+ steps:
97
+ - log_retry:
98
+ call: sys.log
99
+ args:
100
+ text: '${"Pass " + pass_id + " - Empty dispatch (" + string(consecutive_empty_dispatches) + "/3). Retrying..."}'
101
+ - wait_short:
102
+ call: sys.sleep
103
+ args:
104
+ seconds: 5
105
+ - update_cursor_retry:
106
+ assign:
107
+ # Still advance cursor if it wasn't a strict reroute, to try next date
108
+ - n_cursor: '${if(dispatch_res.body.n_cursor_ignored, n_cursor, n_cursor + 1)}'
109
+ - next_loop_retry:
110
+ next: sequential_date_loop
111
+
70
112
  - finish:
71
113
  return: "Pipeline Execution Satiated and Complete"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.309",
3
+ "version": "1.0.311",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [