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}
|
|
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
|
|
59
|
+
logger.log('ERROR', `[Dispatcher] โ Date range is empty.`);
|
|
64
60
|
return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
|
|
65
61
|
}
|
|
66
62
|
|
|
67
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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('
|
|
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
|
-
//
|
|
140
|
-
const
|
|
141
|
-
const
|
|
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:
|
|
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
|
-
//
|
|
52
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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;
|
|
97
|
+
return;
|
|
99
98
|
}
|
|
100
|
-
throw err;
|
|
99
|
+
throw err;
|
|
101
100
|
}
|
|
102
101
|
}
|
|
103
102
|
|