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
|
|
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
|
-
|
|
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
|
-
|
|
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('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
|
-
//
|
|
140
|
-
const
|
|
141
|
-
|
|
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:
|
|
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
|
|
|
@@ -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
|
-
-
|
|
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"
|