bulltrackers-module 1.0.324 → 1.0.325
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,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FILENAME: computation-system/helpers/computation_dispatcher.js
|
|
3
|
-
* PURPOSE: Sequential Cursor-Based Dispatcher
|
|
4
|
-
*
|
|
3
|
+
* PURPOSE: Sequential Cursor-Based Dispatcher.
|
|
4
|
+
* BEHAVIOR: Dispatch -> Wait ETA -> Next Date.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { getExpectedDateStrings, getEarliestDataDates, normalizeName, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils.js');
|
|
@@ -34,7 +34,7 @@ async function filterActiveTasks(db, date, pass, tasks) {
|
|
|
34
34
|
data.completedAt &&
|
|
35
35
|
(Date.now() - new Date(data.completedAt).getTime() < 60 * 1000);
|
|
36
36
|
|
|
37
|
-
if (isActive || isJustFinished) return null;
|
|
37
|
+
if (isActive || isJustFinished) return null; // Filter out
|
|
38
38
|
}
|
|
39
39
|
return t;
|
|
40
40
|
});
|
|
@@ -50,18 +50,13 @@ async function attemptSimHashResolution(dependencies, date, tasks, dailyStatus,
|
|
|
50
50
|
const { db, logger } = dependencies;
|
|
51
51
|
const resolvedTasks = [];
|
|
52
52
|
const remainingTasks = [];
|
|
53
|
-
|
|
54
|
-
// Cache for SimHashes to avoid redundant DB lookups in loop
|
|
55
53
|
const simHashCache = new Map();
|
|
56
54
|
|
|
57
55
|
for (const task of tasks) {
|
|
58
|
-
// Only apply to Re-Runs (Hash Mismatches), not fresh runs (Missing Data)
|
|
59
56
|
const currentStatus = dailyStatus ? dailyStatus[task.name] : null;
|
|
60
57
|
const manifestItem = manifestMap.get(normalizeName(task.name));
|
|
61
58
|
|
|
62
59
|
if (currentStatus && currentStatus.simHash && manifestItem) {
|
|
63
|
-
|
|
64
|
-
// 1. Get the SimHash for the NEW code (from Registry)
|
|
65
60
|
let newSimHash = simHashCache.get(manifestItem.hash);
|
|
66
61
|
if (!newSimHash) {
|
|
67
62
|
const simDoc = await db.collection('system_simhash_registry').doc(manifestItem.hash).get();
|
|
@@ -71,13 +66,12 @@ async function attemptSimHashResolution(dependencies, date, tasks, dailyStatus,
|
|
|
71
66
|
}
|
|
72
67
|
}
|
|
73
68
|
|
|
74
|
-
// 2. Compare
|
|
75
69
|
if (newSimHash && newSimHash === currentStatus.simHash) {
|
|
76
70
|
resolvedTasks.push({
|
|
77
71
|
name: task.name,
|
|
78
72
|
hash: manifestItem.hash,
|
|
79
73
|
simHash: newSimHash,
|
|
80
|
-
prevStatus: currentStatus
|
|
74
|
+
prevStatus: currentStatus
|
|
81
75
|
});
|
|
82
76
|
continue;
|
|
83
77
|
}
|
|
@@ -85,30 +79,24 @@ async function attemptSimHashResolution(dependencies, date, tasks, dailyStatus,
|
|
|
85
79
|
remainingTasks.push(task);
|
|
86
80
|
}
|
|
87
81
|
|
|
88
|
-
// 3. Apply Updates for Stable Tasks
|
|
89
82
|
if (resolvedTasks.length > 0) {
|
|
90
83
|
const updatePayload = {};
|
|
91
|
-
|
|
92
84
|
resolvedTasks.forEach(t => {
|
|
93
|
-
// [FIXED] Construct full nested object to avoid dot-notation issues with .set()
|
|
94
|
-
// We merge existing data (like resultHash) so we don't lose the valid calculation output
|
|
95
85
|
updatePayload[t.name] = {
|
|
96
|
-
...(t.prevStatus || {}),
|
|
97
|
-
hash: t.hash,
|
|
98
|
-
simHash: t.simHash,
|
|
86
|
+
...(t.prevStatus || {}),
|
|
87
|
+
hash: t.hash,
|
|
88
|
+
simHash: t.simHash,
|
|
99
89
|
reason: 'SimHash Stable (Auto-Resolved)',
|
|
100
90
|
lastUpdated: new Date().toISOString()
|
|
101
91
|
};
|
|
102
92
|
});
|
|
103
|
-
|
|
104
|
-
// Use set with merge: true. Now that keys are "clean" (no dots),
|
|
105
|
-
// objects will merge correctly into the document structure.
|
|
106
93
|
await db.collection('computation_status').doc(date).set(updatePayload, { merge: true });
|
|
107
94
|
logger.log('INFO', `[SimHash] ⏩ Fast-forwarded ${resolvedTasks.length} tasks for ${date} (Logic Unchanged).`);
|
|
108
95
|
}
|
|
109
96
|
|
|
110
97
|
return remainingTasks;
|
|
111
98
|
}
|
|
99
|
+
|
|
112
100
|
// =============================================================================
|
|
113
101
|
// HELPER: Stable Session Management (Solves Cursor Shifting)
|
|
114
102
|
// =============================================================================
|
|
@@ -117,36 +105,22 @@ async function getStableDateSession(config, dependencies, passToRun, dateLimitSt
|
|
|
117
105
|
const sessionId = `pass_${passToRun}_${dateLimitStr.replace(/-/g, '')}`;
|
|
118
106
|
const sessionRef = db.collection('dispatcher_sessions').doc(sessionId);
|
|
119
107
|
|
|
120
|
-
// 1. Try to Load Session
|
|
121
108
|
if (!forceRebuild) {
|
|
122
109
|
const sessionSnap = await sessionRef.get();
|
|
123
110
|
if (sessionSnap.exists) {
|
|
124
111
|
const data = sessionSnap.data();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
logger.log('INFO', `[Session] 📂 Loaded stable session for Pass ${passToRun} (${data.dates.length} dates).`);
|
|
112
|
+
if ((Date.now() - new Date(data.createdAt).getTime()) < SESSION_CACHE_DURATION_MS) {
|
|
113
|
+
// logger.log('INFO', `[Session] 📂 Loaded stable session for Pass ${passToRun}.`);
|
|
128
114
|
return data.dates;
|
|
129
115
|
}
|
|
130
116
|
}
|
|
131
117
|
}
|
|
132
118
|
|
|
133
|
-
// 2. Rebuild Session (Expensive Scan)
|
|
134
119
|
logger.log('INFO', `[Session] 🔄 Rebuilding dispatch session for Pass ${passToRun}...`);
|
|
135
120
|
const earliestDates = await getEarliestDataDates(config, dependencies);
|
|
136
121
|
const allDates = getExpectedDateStrings(earliestDates.absoluteEarliest, new Date(dateLimitStr + 'T00:00:00Z'));
|
|
137
122
|
|
|
138
|
-
|
|
139
|
-
// Optimization: We add ALL dates to the list. The dispatcher checks them individually.
|
|
140
|
-
// Why? Because if we pre-filter here, we repeat the work of the dispatcher.
|
|
141
|
-
// Better: Store the plain list of dates sorted descending (newest first usually better for backfills, ascending for standard).
|
|
142
|
-
// Let's stick to Ascending (oldest first) as standard.
|
|
143
|
-
|
|
144
|
-
await sessionRef.set({
|
|
145
|
-
dates: allDates,
|
|
146
|
-
createdAt: new Date().toISOString(),
|
|
147
|
-
configHash: dateLimitStr // Simple versioning
|
|
148
|
-
});
|
|
149
|
-
|
|
123
|
+
await sessionRef.set({ dates: allDates, createdAt: new Date().toISOString(), configHash: dateLimitStr });
|
|
150
124
|
return allDates;
|
|
151
125
|
}
|
|
152
126
|
|
|
@@ -157,9 +131,9 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
157
131
|
const { logger, db } = dependencies;
|
|
158
132
|
const pubsubUtils = new PubSubUtils(dependencies);
|
|
159
133
|
|
|
160
|
-
const passToRun = String(reqBody.pass ||
|
|
134
|
+
const passToRun = String(reqBody.pass || "1");
|
|
161
135
|
const targetCursorN = parseInt(reqBody.cursorIndex || 1);
|
|
162
|
-
const dateLimitStr = reqBody.date ||
|
|
136
|
+
const dateLimitStr = reqBody.date || "2025-01-01";
|
|
163
137
|
const forceRebuild = reqBody.forceRebuild === true;
|
|
164
138
|
|
|
165
139
|
const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
|
|
@@ -167,38 +141,26 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
167
141
|
const calcsInThisPass = passes[passToRun] || [];
|
|
168
142
|
const manifestWeightMap = new Map(computationManifest.map(c => [normalizeName(c.name), c.weight || 1.0]));
|
|
169
143
|
|
|
170
|
-
if (!calcsInThisPass.length) {
|
|
171
|
-
return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
|
|
172
|
-
}
|
|
144
|
+
if (!calcsInThisPass.length) return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
|
|
173
145
|
|
|
174
|
-
// 1. Get Stable Date List
|
|
146
|
+
// 1. Get Stable Date List
|
|
175
147
|
const sessionDates = await getStableDateSession(config, dependencies, passToRun, dateLimitStr, forceRebuild);
|
|
176
|
-
|
|
177
|
-
if (!sessionDates || sessionDates.length === 0) {
|
|
178
|
-
return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
|
|
179
|
-
}
|
|
148
|
+
if (!sessionDates || sessionDates.length === 0) return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
|
|
180
149
|
|
|
181
|
-
// 2. Select Date
|
|
150
|
+
// 2. Select Date
|
|
182
151
|
let selectedDate = null;
|
|
183
152
|
let selectedTasks = [];
|
|
184
|
-
let isReroute = false;
|
|
185
|
-
let isSweep = false;
|
|
186
153
|
|
|
187
|
-
// Check bounds
|
|
188
154
|
if (targetCursorN <= sessionDates.length) {
|
|
189
|
-
// Normal Operation
|
|
190
155
|
selectedDate = sessionDates[targetCursorN - 1];
|
|
191
156
|
} else {
|
|
192
|
-
// End of list
|
|
193
157
|
return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
|
|
194
158
|
}
|
|
195
159
|
|
|
196
|
-
// 3. Analyze SPECIFIC Date
|
|
197
|
-
// We only fetch status for the ONE date we are looking at + context
|
|
160
|
+
// 3. Analyze SPECIFIC Date
|
|
198
161
|
if (selectedDate) {
|
|
199
|
-
// A. Fetch Context
|
|
200
|
-
const needsHistory = calcsInThisPass.some(c => c.isHistorical);
|
|
201
162
|
const earliestDates = await getEarliestDataDates(config, dependencies);
|
|
163
|
+
const needsHistory = calcsInThisPass.some(c => c.isHistorical);
|
|
202
164
|
|
|
203
165
|
let prevDailyStatusPromise = Promise.resolve(null);
|
|
204
166
|
if (needsHistory) {
|
|
@@ -219,51 +181,42 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
219
181
|
const report = analyzeDateExecution(selectedDate, calcsInThisPass, availability.status, dailyStatus, manifestMap, prevDailyStatus);
|
|
220
182
|
let rawTasks = [...report.runnable, ...report.reRuns];
|
|
221
183
|
|
|
222
|
-
// B. Apply SimHash Resolution (Problem #1)
|
|
223
184
|
if (rawTasks.length > 0) {
|
|
224
185
|
rawTasks = await attemptSimHashResolution(dependencies, selectedDate, rawTasks, dailyStatus, manifestMap);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// C. Apply Ledger Filter (Problem #2)
|
|
228
|
-
if (rawTasks.length > 0) {
|
|
186
|
+
|
|
187
|
+
// Ledger Filter: Removes tasks that are already running
|
|
229
188
|
selectedTasks = await filterActiveTasks(db, selectedDate, passToRun, rawTasks);
|
|
230
189
|
}
|
|
231
190
|
|
|
232
|
-
//
|
|
191
|
+
// OOM / High-Mem Reroute Check
|
|
233
192
|
if (selectedTasks.length > 0) {
|
|
234
193
|
const reroutes = await getHighMemReroutes(db, selectedDate, passToRun, selectedTasks);
|
|
235
194
|
if (reroutes.length > 0) {
|
|
236
195
|
selectedTasks = reroutes;
|
|
237
|
-
isReroute = true;
|
|
238
196
|
}
|
|
239
197
|
}
|
|
240
|
-
} else {
|
|
241
|
-
logger.log('WARN', `[Dispatcher] Date ${selectedDate} skipped (Data Unavailable).`);
|
|
242
198
|
}
|
|
243
199
|
}
|
|
244
200
|
|
|
245
201
|
// 4. Dispatch Logic
|
|
246
202
|
if (selectedTasks.length === 0) {
|
|
247
|
-
//
|
|
248
|
-
// CRITICAL: We return dispatched: 0, but n_cursor_ignored: FALSE.
|
|
249
|
-
// This tells workflow to increment cursor and check the next date in the Stable Session.
|
|
203
|
+
// Return 0 dispatched, FALSE cursor ignored -> Move to NEXT date immediately.
|
|
250
204
|
return {
|
|
251
205
|
status: 'CONTINUE_PASS',
|
|
252
206
|
dateProcessed: selectedDate,
|
|
253
207
|
dispatched: 0,
|
|
254
|
-
n_cursor_ignored: false,
|
|
208
|
+
n_cursor_ignored: false,
|
|
255
209
|
etaSeconds: 0,
|
|
256
210
|
remainingDates: sessionDates.length - targetCursorN
|
|
257
211
|
};
|
|
258
212
|
}
|
|
259
213
|
|
|
260
|
-
// 5.
|
|
214
|
+
// 5. Send Tasks
|
|
261
215
|
const totalweight = selectedTasks.reduce((sum, t) => sum + (manifestWeightMap.get(normalizeName(t.name)) || 1.0), 0);
|
|
262
216
|
const currentDispatchId = crypto.randomUUID();
|
|
263
217
|
const etaSeconds = Math.max(20, Math.ceil(totalweight * BASE_SECONDS_PER_WEIGHT_UNIT));
|
|
264
218
|
|
|
265
219
|
const taskDetails = selectedTasks.map(t => `${t.name} (${t.reason})`);
|
|
266
|
-
|
|
267
220
|
logger.log('INFO', `[Dispatcher] ✅ Dispatching ${selectedTasks.length} tasks for ${selectedDate}.`, {
|
|
268
221
|
date: selectedDate,
|
|
269
222
|
pass: passToRun,
|
|
@@ -305,13 +258,14 @@ async function dispatchComputationPass(config, dependencies, computationManifest
|
|
|
305
258
|
}
|
|
306
259
|
await Promise.all(pubPromises);
|
|
307
260
|
|
|
308
|
-
// CRITICAL: We dispatched work.
|
|
309
|
-
//
|
|
261
|
+
// CRITICAL: We dispatched work.
|
|
262
|
+
// We return n_cursor_ignored: FALSE.
|
|
263
|
+
// This tells the workflow to Wait ETA -> Increment Cursor -> Move to Next Date.
|
|
310
264
|
return {
|
|
311
265
|
status: 'CONTINUE_PASS',
|
|
312
266
|
dateProcessed: selectedDate,
|
|
313
267
|
dispatched: selectedTasks.length,
|
|
314
|
-
n_cursor_ignored:
|
|
268
|
+
n_cursor_ignored: false, // FORCE NEXT DATE
|
|
315
269
|
etaSeconds: etaSeconds,
|
|
316
270
|
remainingDates: sessionDates.length - targetCursorN
|
|
317
271
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Cloud Workflows: Precision Cursor-Based Orchestrator
|
|
2
|
-
#
|
|
2
|
+
# SIMPLE MODE: Dispatch -> Wait ETA -> Next Date
|
|
3
3
|
|
|
4
4
|
main:
|
|
5
5
|
params: [input]
|
|
@@ -20,7 +20,6 @@ main:
|
|
|
20
20
|
assign:
|
|
21
21
|
- n_cursor: 1
|
|
22
22
|
- pass_complete: false
|
|
23
|
-
- consecutive_empty_dispatches: 0
|
|
24
23
|
|
|
25
24
|
- sequential_date_loop:
|
|
26
25
|
switch:
|
|
@@ -39,26 +38,25 @@ main:
|
|
|
39
38
|
|
|
40
39
|
- evaluate_dispatch:
|
|
41
40
|
switch:
|
|
41
|
+
# 1. End of Session (Dispatcher reached end of date list)
|
|
42
42
|
- condition: '${dispatch_res.body.status == "MOVE_TO_NEXT_PASS"}'
|
|
43
43
|
assign:
|
|
44
44
|
- pass_complete: true
|
|
45
45
|
|
|
46
|
-
#
|
|
47
|
-
- condition: '${dispatch_res.body.status == "CONTINUE_PASS" and dispatch_res.body.remainingDates == 0}'
|
|
46
|
+
# 2. Satiation Check (Specific to date/logic)
|
|
47
|
+
- condition: '${dispatch_res.body.status == "CONTINUE_PASS" and dispatch_res.body.remainingDates == 0 and dispatch_res.body.dispatched == 0}'
|
|
48
48
|
steps:
|
|
49
49
|
- log_satiation:
|
|
50
50
|
call: sys.log
|
|
51
51
|
args:
|
|
52
|
-
text: '${"Pass " + pass_id + " - ✅ Pass satiated (0 remaining
|
|
52
|
+
text: '${"Pass " + pass_id + " - ✅ Pass satiated (0 remaining). Next pass."}'
|
|
53
53
|
- mark_complete:
|
|
54
54
|
assign:
|
|
55
55
|
- pass_complete: true
|
|
56
56
|
|
|
57
|
+
# 3. Work Dispatched: Wait ETA -> Move Next (Ignored flag is FALSE)
|
|
57
58
|
- condition: '${dispatch_res.body.dispatched > 0}'
|
|
58
59
|
steps:
|
|
59
|
-
- reset_retry_counter:
|
|
60
|
-
assign:
|
|
61
|
-
- consecutive_empty_dispatches: 0
|
|
62
60
|
- wait_for_completion:
|
|
63
61
|
call: sys.sleep
|
|
64
62
|
args:
|
|
@@ -69,26 +67,18 @@ main:
|
|
|
69
67
|
- next_loop_work:
|
|
70
68
|
next: sequential_date_loop
|
|
71
69
|
|
|
70
|
+
# 4. No Work (Clean or Busy): Move Next Immediately
|
|
72
71
|
- condition: '${dispatch_res.body.dispatched == 0}'
|
|
73
72
|
steps:
|
|
74
|
-
-
|
|
73
|
+
- wait_short:
|
|
74
|
+
call: sys.sleep
|
|
75
|
+
args:
|
|
76
|
+
seconds: 2 # Tiny debounce
|
|
77
|
+
- update_cursor_retry:
|
|
75
78
|
assign:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
assign:
|
|
81
|
-
- pass_complete: true
|
|
82
|
-
- condition: '${true}'
|
|
83
|
-
steps:
|
|
84
|
-
- wait_short:
|
|
85
|
-
call: sys.sleep
|
|
86
|
-
args:
|
|
87
|
-
seconds: 5
|
|
88
|
-
- update_cursor_retry:
|
|
89
|
-
assign:
|
|
90
|
-
- n_cursor: '${if(dispatch_res.body.n_cursor_ignored, n_cursor, n_cursor + 1)}'
|
|
91
|
-
- next_loop_retry:
|
|
92
|
-
next: sequential_date_loop
|
|
79
|
+
# Dispatcher sends n_cursor_ignored=false, so we increment.
|
|
80
|
+
- n_cursor: '${if(dispatch_res.body.n_cursor_ignored, n_cursor, n_cursor + 1)}'
|
|
81
|
+
- next_loop_retry:
|
|
82
|
+
next: sequential_date_loop
|
|
93
83
|
- finish:
|
|
94
84
|
return: "Pipeline Execution Satiated and Complete"
|