bulltrackers-module 1.0.328 โ†’ 1.0.330

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.
@@ -2,7 +2,7 @@
2
2
  * FILENAME: computation-system/helpers/computation_dispatcher.js
3
3
  * PURPOSE: Sequential Cursor-Based Dispatcher.
4
4
  * BEHAVIOR: Dispatch -> Wait ETA -> Next Date.
5
- * UPDATED: Added "Zombie Protocol" to auto-recover stale locks.
5
+ * UPDATED: Added "Sweep" Protocol for OOM recovery & High-Mem Verification.
6
6
  */
7
7
 
8
8
  const { getExpectedDateStrings, getEarliestDataDates, normalizeName, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils.js');
@@ -15,16 +15,14 @@ const crypto = require('crypto');
15
15
  const OOM_THRESHOLD_MB = 1500;
16
16
  const BASE_SECONDS_PER_WEIGHT_UNIT = 3;
17
17
  const SESSION_CACHE_DURATION_MS = 1000 * 60 * 30; // 30 Minutes
18
-
19
- // [NEW] Zombie Timeout: Max Cloud Function run is 9m (540s).
20
- // If no heartbeat/start within 15m, it's definitely dead.
21
18
  const STALE_LOCK_THRESHOLD_MS = 1000 * 60 * 15;
22
19
 
23
20
  // =============================================================================
24
- // HELPER: Ledger Awareness (Prevents Race Conditions & Clears Zombies)
21
+ // HELPER: Ledger Awareness
25
22
  // =============================================================================
26
- async function filterActiveTasks(db, date, pass, tasks, logger) {
23
+ async function filterActiveTasks(db, date, pass, tasks, logger, forceRun = false) {
27
24
  if (!tasks || tasks.length === 0) return [];
25
+ if (forceRun) return tasks; // Bypass check for Sweep Mode
28
26
 
29
27
  const checkPromises = tasks.map(async (t) => {
30
28
  const taskName = normalizeName(t.name);
@@ -37,7 +35,6 @@ async function filterActiveTasks(db, date, pass, tasks, logger) {
37
35
 
38
36
  // 1. ZOMBIE CHECK (Recover Stale Locks)
39
37
  if (isActive) {
40
- // Prefer heartbeat, fall back to start time
41
38
  const lastActivityTime = data.telemetry?.lastHeartbeat
42
39
  ? new Date(data.telemetry.lastHeartbeat).getTime()
43
40
  : (data.startedAt ? new Date(data.startedAt).getTime() : 0);
@@ -45,25 +42,18 @@ async function filterActiveTasks(db, date, pass, tasks, logger) {
45
42
  const timeSinceActive = Date.now() - lastActivityTime;
46
43
 
47
44
  if (timeSinceActive > STALE_LOCK_THRESHOLD_MS) {
48
- if (logger) {
49
- logger.log('WARN', `[Dispatcher] ๐ŸงŸ Breaking stale lock for ${taskName}. Inactive for ${(timeSinceActive/60000).toFixed(1)} mins.`);
50
- }
51
- // Return task (Re-dispatching it will overwrite the old lock in Firestore)
45
+ if (logger) logger.log('WARN', `[Dispatcher] ๐ŸงŸ Breaking stale lock for ${taskName}.`);
52
46
  return t;
53
47
  }
54
-
55
- // If distinct and recent, filter it out (let it run)
56
48
  return null;
57
49
  }
58
50
 
59
51
  // 2. GHOST CHECK (Debounce immediate re-runs)
60
- // If it finished less than 1 minute ago, don't re-dispatch immediately
61
- // (prevents double-tap if latency is high)
62
52
  const isJustFinished = data.status === 'COMPLETED' &&
63
53
  data.completedAt &&
64
54
  (Date.now() - new Date(data.completedAt).getTime() < 60 * 1000);
65
55
 
66
- if (isJustFinished) return null; // Filter out
56
+ if (isJustFinished) return null;
67
57
  }
68
58
  return t;
69
59
  });
@@ -73,7 +63,7 @@ async function filterActiveTasks(db, date, pass, tasks, logger) {
73
63
  }
74
64
 
75
65
  // =============================================================================
76
- // HELPER: SimHash Stability (Solves Live Analysis Disconnect)
66
+ // HELPER: SimHash Stability
77
67
  // =============================================================================
78
68
  async function attemptSimHashResolution(dependencies, date, tasks, dailyStatus, manifestMap) {
79
69
  const { db, logger } = dependencies;
@@ -120,15 +110,12 @@ async function attemptSimHashResolution(dependencies, date, tasks, dailyStatus,
120
110
  };
121
111
  });
122
112
  await db.collection('computation_status').doc(date).set(updatePayload, { merge: true });
123
- logger.log('INFO', `[SimHash] โฉ Fast-forwarded ${resolvedTasks.length} tasks for ${date} (Logic Unchanged).`);
113
+ logger.log('INFO', `[SimHash] โฉ Fast-forwarded ${resolvedTasks.length} tasks for ${date}.`);
124
114
  }
125
115
 
126
116
  return remainingTasks;
127
117
  }
128
118
 
129
- // =============================================================================
130
- // HELPER: Stable Session Management (Solves Cursor Shifting)
131
- // =============================================================================
132
119
  async function getStableDateSession(config, dependencies, passToRun, dateLimitStr, forceRebuild) {
133
120
  const { db, logger } = dependencies;
134
121
  const sessionId = `pass_${passToRun}_${dateLimitStr.replace(/-/g, '')}`;
@@ -153,29 +140,164 @@ async function getStableDateSession(config, dependencies, passToRun, dateLimitSt
153
140
  }
154
141
 
155
142
  // =============================================================================
156
- // MAIN DISPATCHER
143
+ // MAIN ENTRY POINT
157
144
  // =============================================================================
158
145
  async function dispatchComputationPass(config, dependencies, computationManifest, reqBody = {}) {
146
+ const action = reqBody.action || 'DISPATCH'; // DISPATCH, VERIFY, SWEEP
147
+
148
+ if (action === 'VERIFY') {
149
+ return handlePassVerification(config, dependencies, computationManifest, reqBody);
150
+ }
151
+ else if (action === 'SWEEP') {
152
+ return handleSweepDispatch(config, dependencies, computationManifest, reqBody);
153
+ }
154
+
155
+ return handleStandardDispatch(config, dependencies, computationManifest, reqBody);
156
+ }
157
+
158
+ // =============================================================================
159
+ // LOGIC: Verify Pass Completion
160
+ // =============================================================================
161
+ async function handlePassVerification(config, dependencies, computationManifest, reqBody) {
162
+ const { logger } = dependencies;
163
+ const passToRun = String(reqBody.pass || "1");
164
+ const dateLimitStr = reqBody.date || "2025-01-01";
165
+
166
+ logger.log('INFO', `[Verify] ๐Ÿงน Sweeping Pass ${passToRun} for unfinished work...`);
167
+
168
+ const sessionDates = await getStableDateSession(config, dependencies, passToRun, dateLimitStr, false);
169
+ const passes = groupByPass(computationManifest);
170
+ const calcsInPass = passes[passToRun] || [];
171
+ const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
172
+ const weightMap = new Map(computationManifest.map(c => [normalizeName(c.name), c.weight || 1.0]));
173
+
174
+ const missingTasks = [];
175
+
176
+ // Optimize: Batch fetch statuses if possible, but for now loop is safer for memory
177
+ // In production, we might want p-limit here.
178
+ for (const date of sessionDates) {
179
+ const [dailyStatus, availability] = await Promise.all([
180
+ fetchComputationStatus(date, config, dependencies),
181
+ checkRootDataAvailability(date, config, dependencies, DEFINITIVE_EARLIEST_DATES)
182
+ ]);
183
+
184
+ // Need previous status for historical calcs
185
+ let prevDailyStatus = null;
186
+ if (calcsInPass.some(c => c.isHistorical)) {
187
+ const prevD = new Date(date + 'T00:00:00Z');
188
+ prevD.setUTCDate(prevD.getUTCDate() - 1);
189
+ prevDailyStatus = await fetchComputationStatus(prevD.toISOString().slice(0, 10), config, dependencies);
190
+ }
191
+
192
+ const report = analyzeDateExecution(date, calcsInPass, availability ? availability.status : {}, dailyStatus, manifestMap, prevDailyStatus);
193
+
194
+ // We only care about Runnable (New) or ReRuns (Changed/Failed)
195
+ // We ignore Blocked (impossible to run) and Impossible (permanent fail)
196
+ const pending = [...report.runnable, ...report.reRuns];
197
+
198
+ if (pending.length > 0) {
199
+ // Calculate ETA
200
+ const totalWeight = pending.reduce((sum, t) => sum + (weightMap.get(normalizeName(t.name)) || 1.0), 0);
201
+ const eta = Math.max(30, Math.ceil(totalWeight * BASE_SECONDS_PER_WEIGHT_UNIT));
202
+
203
+ missingTasks.push({
204
+ date: date,
205
+ taskCount: pending.length,
206
+ eta: eta
207
+ });
208
+ }
209
+ }
210
+
211
+ logger.log('INFO', `[Verify] Found ${missingTasks.length} dates with pending work.`);
212
+ return { missingTasks };
213
+ }
214
+
215
+ // =============================================================================
216
+ // LOGIC: Sweep Dispatch (Forced High-Mem)
217
+ // =============================================================================
218
+ async function handleSweepDispatch(config, dependencies, computationManifest, reqBody) {
159
219
  const { logger, db } = dependencies;
160
- const pubsubUtils = new PubSubUtils(dependencies);
220
+ const pubsubUtils = new PubSubUtils(dependencies);
221
+ const passToRun = String(reqBody.pass || "1");
222
+ const date = reqBody.date;
223
+
224
+ if (!date) throw new Error('Sweep dispatch requires date');
225
+
226
+ const passes = groupByPass(computationManifest);
227
+ const calcsInPass = passes[passToRun] || [];
228
+ const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
229
+
230
+ // 1. Analyze specific date
231
+ const [dailyStatus, availability] = await Promise.all([
232
+ fetchComputationStatus(date, config, dependencies),
233
+ checkRootDataAvailability(date, config, dependencies, DEFINITIVE_EARLIEST_DATES)
234
+ ]);
161
235
 
162
- const passToRun = String(reqBody.pass || "1");
236
+ // Previous Status Fetch (simplified for brevity, assume historical dependency check works or fails safe)
237
+ let prevDailyStatus = null;
238
+ if (calcsInPass.some(c => c.isHistorical)) {
239
+ const prevD = new Date(date + 'T00:00:00Z');
240
+ prevD.setUTCDate(prevD.getUTCDate() - 1);
241
+ prevDailyStatus = await fetchComputationStatus(prevD.toISOString().slice(0, 10), config, dependencies);
242
+ }
243
+
244
+ const report = analyzeDateExecution(date, calcsInPass, availability ? availability.status : {}, dailyStatus, manifestMap, prevDailyStatus);
245
+ const pending = [...report.runnable, ...report.reRuns];
246
+
247
+ if (pending.length === 0) {
248
+ logger.log('INFO', `[Sweep] ${date} is clean. No dispatch.`);
249
+ return { dispatched: 0 };
250
+ }
251
+
252
+ // 2. FORCE High Mem & Skip Zombie Check
253
+ // We do NOT filterActiveTasks because we assume they might be OOM'd zombies
254
+ // We do NOT check resource tier from previous runs, we FORCE 'high-mem'
255
+ const currentDispatchId = crypto.randomUUID();
256
+
257
+ const tasksPayload = pending.map(t => ({
258
+ ...t,
259
+ action: 'RUN_COMPUTATION_DATE',
260
+ computation: t.name,
261
+ date: date,
262
+ pass: passToRun,
263
+ dispatchId: currentDispatchId,
264
+ triggerReason: 'SWEEP_RECOVERY',
265
+ resources: 'high-mem' // FORCE
266
+ }));
267
+
268
+ logger.log('WARN', `[Sweep] ๐Ÿงน Forcing ${tasksPayload.length} tasks to HIGH-MEM for ${date}.`);
269
+
270
+ await pubsubUtils.batchPublishTasks(dependencies, {
271
+ topicName: config.computationTopicHighMem || 'computation-tasks-highmem',
272
+ tasks: tasksPayload,
273
+ taskType: `pass-${passToRun}-sweep`
274
+ });
275
+
276
+ return { dispatched: tasksPayload.length };
277
+ }
278
+
279
+ // =============================================================================
280
+ // LOGIC: Standard Dispatch (Original)
281
+ // =============================================================================
282
+ async function handleStandardDispatch(config, dependencies, computationManifest, reqBody) {
283
+ const { logger, db } = dependencies;
284
+ const pubsubUtils = new PubSubUtils(dependencies);
285
+
286
+ const passToRun = String(reqBody.pass || "1");
163
287
  const targetCursorN = parseInt(reqBody.cursorIndex || 1);
164
- const dateLimitStr = reqBody.date || "2025-01-01";
165
- const forceRebuild = reqBody.forceRebuild === true;
288
+ const dateLimitStr = reqBody.date || "2025-01-01";
289
+ const forceRebuild = reqBody.forceRebuild === true;
166
290
 
167
- const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
168
- const passes = groupByPass(computationManifest);
291
+ const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
292
+ const passes = groupByPass(computationManifest);
169
293
  const calcsInThisPass = passes[passToRun] || [];
170
294
  const manifestWeightMap = new Map(computationManifest.map(c => [normalizeName(c.name), c.weight || 1.0]));
171
295
 
172
296
  if (!calcsInThisPass.length) return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
173
297
 
174
- // 1. Get Stable Date List
175
298
  const sessionDates = await getStableDateSession(config, dependencies, passToRun, dateLimitStr, forceRebuild);
176
299
  if (!sessionDates || sessionDates.length === 0) return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
177
300
 
178
- // 2. Select Date
179
301
  let selectedDate = null;
180
302
  let selectedTasks = [];
181
303
 
@@ -185,13 +307,10 @@ async function dispatchComputationPass(config, dependencies, computationManifest
185
307
  return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
186
308
  }
187
309
 
188
- // 3. Analyze SPECIFIC Date
189
310
  if (selectedDate) {
190
311
  const earliestDates = await getEarliestDataDates(config, dependencies);
191
- const needsHistory = calcsInThisPass.some(c => c.isHistorical);
192
-
193
312
  let prevDailyStatusPromise = Promise.resolve(null);
194
- if (needsHistory) {
313
+ if (calcsInThisPass.some(c => c.isHistorical)) {
195
314
  const prevD = new Date(selectedDate + 'T00:00:00Z');
196
315
  prevD.setUTCDate(prevD.getUTCDate() - 1);
197
316
  if (prevD >= earliestDates.absoluteEarliest) {
@@ -211,22 +330,16 @@ async function dispatchComputationPass(config, dependencies, computationManifest
211
330
 
212
331
  if (rawTasks.length > 0) {
213
332
  rawTasks = await attemptSimHashResolution(dependencies, selectedDate, rawTasks, dailyStatus, manifestMap);
214
-
215
- // [UPDATED] Pass logger to filterActiveTasks for zombie warnings
216
333
  selectedTasks = await filterActiveTasks(db, selectedDate, passToRun, rawTasks, logger);
217
334
  }
218
335
 
219
- // OOM / High-Mem Reroute Check
220
336
  if (selectedTasks.length > 0) {
221
337
  const reroutes = await getHighMemReroutes(db, selectedDate, passToRun, selectedTasks);
222
- if (reroutes.length > 0) {
223
- selectedTasks = reroutes;
224
- }
338
+ if (reroutes.length > 0) selectedTasks = reroutes;
225
339
  }
226
340
  }
227
341
  }
228
342
 
229
- // 4. Dispatch Logic
230
343
  if (selectedTasks.length === 0) {
231
344
  return {
232
345
  status: 'CONTINUE_PASS',
@@ -238,31 +351,18 @@ async function dispatchComputationPass(config, dependencies, computationManifest
238
351
  };
239
352
  }
240
353
 
241
- // 5. Send Tasks
242
354
  const totalweight = selectedTasks.reduce((sum, t) => sum + (manifestWeightMap.get(normalizeName(t.name)) || 1.0), 0);
243
355
  const currentDispatchId = crypto.randomUUID();
244
356
  const etaSeconds = Math.max(20, Math.ceil(totalweight * BASE_SECONDS_PER_WEIGHT_UNIT));
245
357
 
246
358
  const taskDetails = selectedTasks.map(t => `${t.name} (${t.reason})`);
247
359
  logger.log('INFO', `[Dispatcher] โœ… Dispatching ${selectedTasks.length} tasks for ${selectedDate}.`, {
248
- date: selectedDate,
249
- pass: passToRun,
250
- dispatchedCount: selectedTasks.length,
251
- cursor: targetCursorN,
252
- etaSeconds: etaSeconds,
253
- dispatchId: currentDispatchId,
254
- tasks: taskDetails
360
+ date: selectedDate, pass: passToRun, dispatchedCount: selectedTasks.length, etaSeconds, dispatchId: currentDispatchId
255
361
  });
256
362
 
257
363
  const mapToTaskPayload = (t) => ({
258
- ...t,
259
- action: 'RUN_COMPUTATION_DATE',
260
- computation: t.name,
261
- date: selectedDate,
262
- pass: passToRun,
263
- dispatchId: currentDispatchId,
264
- triggerReason: t.reason,
265
- resources: t.resources || 'standard'
364
+ ...t, action: 'RUN_COMPUTATION_DATE', computation: t.name, date: selectedDate, pass: passToRun,
365
+ dispatchId: currentDispatchId, triggerReason: t.reason, resources: t.resources || 'standard'
266
366
  });
267
367
 
268
368
  const standardTasks = selectedTasks.filter(t => t.resources !== 'high-mem').map(mapToTaskPayload);
@@ -301,15 +401,12 @@ async function getHighMemReroutes(db, date, pass, tasks) {
301
401
  const name = normalizeName(task.name);
302
402
  const ledgerPath = `computation_audit_ledger/${date}/passes/${pass}/tasks/${name}`;
303
403
  const doc = await db.doc(ledgerPath).get();
304
-
305
404
  if (doc.exists) {
306
405
  const data = doc.data();
307
406
  const isOOM = (data.status === 'FAILED' || data.status === 'CRASH') &&
308
407
  (data.resourceTier !== 'high-mem') &&
309
408
  ((data.peakMemoryMB > OOM_THRESHOLD_MB) || (data.error && /memory/i.test(data.error.message)));
310
- if (isOOM) {
311
- reroutes.push({ ...task, resources: 'high-mem' });
312
- }
409
+ if (isOOM) reroutes.push({ ...task, resources: 'high-mem' });
313
410
  }
314
411
  }
315
412
  return reroutes;
@@ -3,4 +3,4 @@
3
3
  * PURPOSE: Master override for BuildReporter.
4
4
  * Increment this string to force a full re-analysis regardless of code or data stability.
5
5
  */
6
- module.exports = "reporter-epoch-2";
6
+ module.exports = "reporter-epoch-3";
@@ -1,5 +1,6 @@
1
1
  # Cloud Workflows: Precision Cursor-Based Orchestrator
2
2
  # SIMPLE MODE: Dispatch -> Wait ETA -> Next Date
3
+ # UPGRADED: Added "Sweep & Verify" Step for High-Mem Recovery
3
4
 
4
5
  main:
5
6
  params: [input]
@@ -49,7 +50,7 @@ main:
49
50
  - log_satiation:
50
51
  call: sys.log
51
52
  args:
52
- text: '${"Pass " + pass_id + " - โœ… Pass satiated (0 remaining). Next pass."}'
53
+ text: '${"Pass " + pass_id + " - โœ… Pass satiated (0 remaining). Starting Verification Sweep."}'
53
54
  - mark_complete:
54
55
  assign:
55
56
  - pass_complete: true
@@ -80,5 +81,43 @@ main:
80
81
  - n_cursor: '${if(dispatch_res.body.n_cursor_ignored, n_cursor, n_cursor + 1)}'
81
82
  - next_loop_retry:
82
83
  next: sequential_date_loop
84
+
85
+ # --- NEW STEP: VERIFICATION & SWEEP ---
86
+ - verify_pass_completion:
87
+ call: http.post
88
+ args:
89
+ url: '${"https://europe-west1-" + project + ".cloudfunctions.net/computation-pass-" + pass_id}'
90
+ body:
91
+ action: 'VERIFY'
92
+ pass: '${pass_id}'
93
+ date: '${date_to_run}'
94
+ auth: { type: OIDC }
95
+ result: verify_res
96
+
97
+ - process_sweep_tasks:
98
+ for:
99
+ value: sweep_task
100
+ in: '${verify_res.body.missingTasks}'
101
+ steps:
102
+ - log_sweep:
103
+ call: sys.log
104
+ args:
105
+ text: '${"๐Ÿงน SWEEP: Disposing " + sweep_task.taskCount + " high-mem tasks for " + sweep_task.date + ". Wait: " + sweep_task.eta + "s"}'
106
+
107
+ - dispatch_force_sweep:
108
+ call: http.post
109
+ args:
110
+ url: '${"https://europe-west1-" + project + ".cloudfunctions.net/computation-pass-" + pass_id}'
111
+ body:
112
+ action: 'SWEEP'
113
+ pass: '${pass_id}'
114
+ date: '${sweep_task.date}'
115
+ auth: { type: OIDC }
116
+
117
+ - wait_sweep_completion:
118
+ call: sys.sleep
119
+ args:
120
+ seconds: '${int(sweep_task.eta)}'
121
+
83
122
  - finish:
84
- return: "Pipeline Execution Satiated and Complete"
123
+ return: "Pipeline Execution Satiated and Verified"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.328",
3
+ "version": "1.0.330",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [