bulltrackers-module 1.0.310 โ†’ 1.0.312

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, triggerReason, and detailed logging requirements.
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');
11
13
 
12
14
  const OOM_THRESHOLD_MB = 1500;
13
15
  const SECONDS_PER_CALC_MARGIN = 25;
@@ -37,108 +39,107 @@ 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";
44
45
 
45
- logger.log('INFO', `[Dispatcher] ๐Ÿš€ STARTING DISPATCH: Pass ${passToRun}, Cursor ${targetCursorN}, Limit ${dateLimitStr}`);
46
-
47
46
  const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
48
47
  const passes = groupByPass(computationManifest);
49
48
  const calcsInThisPass = passes[passToRun] || [];
50
49
 
51
50
  if (!calcsInThisPass.length) {
52
- logger.log('WARN', `[Dispatcher] ๐Ÿ›‘ No calculations found for Pass ${passToRun}. Moving to next pass.`);
51
+ logger.log('WARN', `[Dispatcher] ๐Ÿ›‘ No calculations found for Pass ${passToRun}.`);
53
52
  return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
54
53
  }
55
54
 
56
- // 2. Discover Discovery Boundaries
57
55
  const earliestDates = await getEarliestDataDates(config, dependencies);
58
- logger.log('INFO', `[Dispatcher] Discovery Boundaries: Earliest=${earliestDates.absoluteEarliest.toISOString().slice(0,10)}, Limit=${dateLimitStr}`);
59
-
60
56
  const allDates = getExpectedDateStrings(earliestDates.absoluteEarliest, new Date(dateLimitStr + 'T00:00:00Z'));
61
57
 
62
58
  if (allDates.length === 0) {
63
- logger.log('ERROR', `[Dispatcher] โŒ Date range is empty. Check if dateLimit is before earliest data.`);
59
+ logger.log('ERROR', `[Dispatcher] โŒ Date range is empty.`);
64
60
  return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
65
61
  }
66
62
 
67
- // 3. Date Scanning Loop
63
+ // 1. Identify all "Dirty" dates (dates that actually have work to do)
68
64
  const dirtyDates = [];
69
- let blockedCount = 0;
70
- let upToDateCount = 0;
71
-
72
- logger.log('INFO', `[Dispatcher] Scanning ${allDates.length} dates for work...`);
73
-
74
65
  for (const d of allDates) {
75
66
  const dailyStatus = await fetchComputationStatus(d, config, dependencies);
76
67
  const availability = await checkRootDataAvailability(d, config, dependencies, DEFINITIVE_EARLIEST_DATES);
77
68
 
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
- }
69
+ if (!availability || !availability.status.hasPrices) continue;
85
70
 
86
71
  const report = analyzeDateExecution(d, calcsInThisPass, availability.status, dailyStatus, manifestMap, null);
87
72
  const tasks = [...report.runnable, ...report.reRuns];
88
73
 
89
74
  if (tasks.length > 0) {
90
- logger.log('INFO', `[Dispatcher] โœจ Found Dirty Date: ${d} (${tasks.length} tasks)`);
91
75
  dirtyDates.push({ date: d, tasks });
92
- } else {
93
- upToDateCount++;
94
76
  }
95
77
  }
96
78
 
97
- logger.log('INFO', `[Dispatcher] Scan Complete: ${dirtyDates.length} dirty, ${upToDateCount} up-to-date, ${blockedCount} blocked/missing data.`);
98
-
99
79
  let selectedDate = null;
100
80
  let selectedTasks = [];
101
81
  let isReroute = false;
102
82
  let isSweep = false;
103
83
 
104
- // 4. Cursor Logic
84
+ // Logic for Reroutes (OOM handling)
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);
108
-
109
88
  if (reroutes.length > 0) {
110
89
  selectedDate = prevEntry.date;
111
90
  selectedTasks = reroutes;
112
91
  isReroute = true;
113
- logger.log('INFO', `[Dispatcher] ๐Ÿ”„ Reroute detected for ${selectedDate}. Retrying same cursor position with High-Mem.`);
114
92
  }
115
93
  }
116
94
 
95
+ // Logic for standard cursor progression
117
96
  if (!selectedDate) {
118
97
  if (targetCursorN <= dirtyDates.length) {
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.`);
109
+ logger.log('INFO', `[Dispatcher] ๐Ÿ Pass ${passToRun} is fully satiated. No work remaining.`);
136
110
  return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0, etaSeconds: 0 };
137
111
  }
138
112
 
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 }));
113
+ // 2. Prepare Payload and Telemetry
114
+ const currentDispatchId = crypto.randomUUID();
115
+ const etaSeconds = Math.max(20, selectedTasks.length * SECONDS_PER_CALC_MARGIN);
116
+ const remainingDatesCount = Math.max(0, dirtyDates.length - targetCursorN);
117
+
118
+ // requirement: condense computations into a log payload
119
+ const computationNames = selectedTasks.map(t => t.name);
120
+
121
+ logger.log('INFO', `[Dispatcher] โœ… Dispatching ${selectedTasks.length} tasks for ${selectedDate}. ETA: ${etaSeconds}s.`, {
122
+ date: selectedDate,
123
+ pass: passToRun,
124
+ dispatchedCount: selectedTasks.length,
125
+ remainingCursorDates: remainingDatesCount,
126
+ etaSeconds: etaSeconds,
127
+ dispatchId: currentDispatchId,
128
+ tasks: computationNames // Condensed into JSON payload
129
+ });
130
+
131
+ const mapToTaskPayload = (t) => ({
132
+ ...t,
133
+ action: 'RUN_COMPUTATION_DATE',
134
+ computation: t.name,
135
+ date: selectedDate,
136
+ pass: passToRun,
137
+ dispatchId: currentDispatchId,
138
+ triggerReason: t.reason
139
+ });
140
+
141
+ const standardTasks = selectedTasks.filter(t => t.resources !== 'high-mem').map(mapToTaskPayload);
142
+ const highMemTasks = selectedTasks.filter(t => t.resources === 'high-mem').map(mapToTaskPayload);
142
143
 
143
144
  const pubPromises = [];
144
145
  if (standardTasks.length > 0) {
@@ -157,16 +158,13 @@ async function dispatchComputationPass(config, dependencies, computationManifest
157
158
  }
158
159
  await Promise.all(pubPromises);
159
160
 
160
- const etaSeconds = Math.max(20, selectedTasks.length * SECONDS_PER_CALC_MARGIN);
161
-
162
- logger.log('INFO', `[Dispatcher] ๐Ÿ›ฐ๏ธ DISPATCHED ${selectedTasks.length} tasks for ${selectedDate}. ETA ${etaSeconds}s.`);
163
-
164
161
  return {
165
162
  status : isSweep ? 'RECOVERY' : 'CONTINUE_PASS',
166
163
  dateProcessed : selectedDate,
167
164
  dispatched : selectedTasks.length,
168
165
  n_cursor_ignored: isReroute,
169
- etaSeconds : etaSeconds
166
+ etaSeconds : etaSeconds,
167
+ remainingDates : remainingDatesCount // For Workflow consumption
170
168
  };
171
169
  }
172
170
 
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.310",
3
+ "version": "1.0.312",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [