bulltrackers-module 1.0.776 → 1.0.778
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,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Scheduler V3.
|
|
2
|
+
* @fileoverview Scheduler V3.4: Enhanced Logging for Verification
|
|
3
3
|
* * * 1. Reconcile: Checks ENTIRE graph for stale hashes, triggers ROOTS if needed.
|
|
4
4
|
* * 2. Purge: Actively removes tasks that don't match the current deployment hash.
|
|
5
5
|
* * 3. Watchdog: Recovers "Zombie" tasks (running but stuck).
|
|
6
|
+
* * 4. Logging: Detailed output for Tombstoned/Existing tasks to verify ETA logic.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
const { CloudTasksClient } = require('@google-cloud/tasks');
|
|
@@ -17,8 +18,8 @@ const config = require('../config/bulltrackers.config');
|
|
|
17
18
|
|
|
18
19
|
// Config
|
|
19
20
|
const CLOUD_TASKS_CONCURRENCY = 20;
|
|
20
|
-
const PLANNING_LOOKBACK_DAYS = 7;
|
|
21
|
-
const PLANNING_LOOKAHEAD_HOURS = 24;
|
|
21
|
+
const PLANNING_LOOKBACK_DAYS = 7;
|
|
22
|
+
const PLANNING_LOOKAHEAD_HOURS = 24;
|
|
22
23
|
const ZOMBIE_THRESHOLD_MINUTES = 15;
|
|
23
24
|
|
|
24
25
|
// Cache singleton instances
|
|
@@ -44,7 +45,6 @@ async function initialize() {
|
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
47
|
* ENTRY POINT 1: The Planner
|
|
47
|
-
* Trigger: Cloud Scheduler -> "0 * * * *" (Hourly)
|
|
48
48
|
*/
|
|
49
49
|
async function planComputations(req, res) {
|
|
50
50
|
try {
|
|
@@ -61,8 +61,6 @@ async function planComputations(req, res) {
|
|
|
61
61
|
|
|
62
62
|
console.log(`[Planner] Reconciling window: ${windowStart.toISOString()} to ${windowEnd.toISOString()}`);
|
|
63
63
|
|
|
64
|
-
// Helper to find Roots for any given computation (Pass 1..N)
|
|
65
|
-
// If a deep node (Pass 3) is stale, we must run its Pass 1 ancestor to regenerate the data.
|
|
66
64
|
const manifestMap = new Map(manifest.map(m => [m.name, m]));
|
|
67
65
|
const getRoots = (entry, visited = new Set()) => {
|
|
68
66
|
if (visited.has(entry.name)) return [];
|
|
@@ -74,7 +72,7 @@ async function planComputations(req, res) {
|
|
|
74
72
|
.flatMap(p => getRoots(p, visited));
|
|
75
73
|
};
|
|
76
74
|
|
|
77
|
-
const tasksToSchedule = new Map();
|
|
75
|
+
const tasksToSchedule = new Map();
|
|
78
76
|
const stats = { checked: 0, scheduled: 0, mismatched: 0, missing: 0 };
|
|
79
77
|
|
|
80
78
|
const targetDates = [];
|
|
@@ -89,9 +87,7 @@ async function planComputations(req, res) {
|
|
|
89
87
|
const dateStr = dateObj.toISOString().split('T')[0];
|
|
90
88
|
const dailyStatus = await stateRepository.getDailyStatus(dateStr);
|
|
91
89
|
|
|
92
|
-
// Iterate ALL computations (not just Pass 1) to find stale nodes
|
|
93
90
|
for (const entry of manifest) {
|
|
94
|
-
// If this specific entry is not scheduled for today, skip it
|
|
95
91
|
if (!shouldRunOnDate(entry.schedule, dateObj)) continue;
|
|
96
92
|
|
|
97
93
|
stats.checked++;
|
|
@@ -107,22 +103,22 @@ async function planComputations(req, res) {
|
|
|
107
103
|
}
|
|
108
104
|
|
|
109
105
|
if (reason) {
|
|
110
|
-
// If entry is stale, we must schedule its ROOT(s) to trigger the chain
|
|
111
106
|
const roots = getRoots(entry);
|
|
112
107
|
|
|
113
108
|
roots.forEach(root => {
|
|
114
|
-
// Unique Task Key: RootName + Date + Hash
|
|
115
|
-
// Including Hash in key ensures we don't dedupe against a DIFFERENT version
|
|
116
109
|
const taskKey = `root-${toKebab(root.originalName)}-${dateStr}-${root.hash}`;
|
|
117
110
|
|
|
118
111
|
if (!tasksToSchedule.has(taskKey)) {
|
|
112
|
+
// Calculate proper ETA (Next valid window)
|
|
113
|
+
const runAt = getNextRunWindow(root.schedule, dateObj);
|
|
114
|
+
|
|
119
115
|
tasksToSchedule.set(taskKey, {
|
|
120
116
|
computation: root.originalName,
|
|
121
117
|
targetDate: dateStr,
|
|
122
|
-
runAtSeconds:
|
|
118
|
+
runAtSeconds: runAt,
|
|
123
119
|
configHash: root.hash,
|
|
124
120
|
queuePath: getQueuePath(),
|
|
125
|
-
reason: `TRIGGERED_BY_${entry.name}_${reason}`
|
|
121
|
+
reason: `TRIGGERED_BY_${entry.name}_${reason}`
|
|
126
122
|
});
|
|
127
123
|
}
|
|
128
124
|
});
|
|
@@ -164,7 +160,6 @@ async function planComputations(req, res) {
|
|
|
164
160
|
|
|
165
161
|
/**
|
|
166
162
|
* ENTRY POINT 2: The Watchdog
|
|
167
|
-
* Trigger: Cloud Scheduler -> "*\/15 * * * *" (Every 15 mins)
|
|
168
163
|
*/
|
|
169
164
|
async function runWatchdog(req, res) {
|
|
170
165
|
try {
|
|
@@ -204,75 +199,48 @@ async function runWatchdog(req, res) {
|
|
|
204
199
|
|
|
205
200
|
async function cleanupOrphanedTasks() {
|
|
206
201
|
const parent = getQueuePath();
|
|
207
|
-
|
|
208
|
-
// Create a map of { kebabName: activeHash } for O(1) lookups
|
|
209
|
-
const activeComputations = new Map(
|
|
210
|
-
manifest.map(m => [toKebab(m.originalName), m.hash])
|
|
211
|
-
);
|
|
212
|
-
|
|
202
|
+
const activeComputations = new Map(manifest.map(m => [toKebab(m.originalName), m.hash]));
|
|
213
203
|
const limit = pLimit(CLOUD_TASKS_CONCURRENCY);
|
|
214
204
|
let deletedCount = 0;
|
|
215
205
|
|
|
216
206
|
try {
|
|
217
207
|
const tasksToDelete = [];
|
|
218
|
-
|
|
219
|
-
// Increase pageSize to 1000 to minimize pagination calls/warnings
|
|
220
|
-
for await (const task of tasksClient.listTasksAsync({
|
|
221
|
-
parent,
|
|
222
|
-
responseView: 'BASIC',
|
|
223
|
-
pageSize: 1000
|
|
224
|
-
})) {
|
|
208
|
+
for await (const task of tasksClient.listTasksAsync({ parent, responseView: 'BASIC', pageSize: 1000 })) {
|
|
225
209
|
const taskNameFull = task.name;
|
|
226
210
|
const taskNameShort = taskNameFull.split('/').pop();
|
|
227
211
|
|
|
228
|
-
// 1.
|
|
212
|
+
// 1. Root Tasks
|
|
229
213
|
const rootMatch = taskNameShort.match(/^root-(.+)-\d{4}-\d{2}-\d{2}-(.+)$/);
|
|
230
|
-
|
|
231
214
|
if (rootMatch) {
|
|
232
215
|
const [_, kebabName, taskHash] = rootMatch;
|
|
233
216
|
const activeHash = activeComputations.get(kebabName);
|
|
234
|
-
|
|
235
|
-
// DELETE IF:
|
|
236
|
-
// A) Computation removed from manifest (!activeHash)
|
|
237
|
-
// B) Hash mismatch (Old deployment/Stale) (activeHash !== taskHash)
|
|
238
|
-
if (!activeHash || activeHash !== taskHash) {
|
|
239
|
-
tasksToDelete.push(taskNameFull);
|
|
240
|
-
}
|
|
217
|
+
if (!activeHash || activeHash !== taskHash) tasksToDelete.push(taskNameFull);
|
|
241
218
|
continue;
|
|
242
219
|
}
|
|
243
220
|
|
|
244
|
-
// 2.
|
|
221
|
+
// 2. Recovery Tasks
|
|
245
222
|
const recoveryMatch = taskNameShort.match(/^recovery-(.+)-\d{4}-\d{2}-\d{2}-/);
|
|
246
|
-
|
|
247
223
|
if (recoveryMatch) {
|
|
248
224
|
const [_, kebabName] = recoveryMatch;
|
|
249
|
-
if (!activeComputations.has(kebabName))
|
|
250
|
-
tasksToDelete.push(taskNameFull);
|
|
251
|
-
}
|
|
225
|
+
if (!activeComputations.has(kebabName)) tasksToDelete.push(taskNameFull);
|
|
252
226
|
}
|
|
253
227
|
}
|
|
254
228
|
|
|
255
229
|
if (tasksToDelete.length === 0) return 0;
|
|
256
|
-
|
|
257
230
|
console.log(`[Planner] 🗑️ Found ${tasksToDelete.length} stale/orphaned tasks. Deleting...`);
|
|
258
231
|
|
|
259
|
-
// 3. Delete in parallel
|
|
260
232
|
await Promise.all(tasksToDelete.map(name => limit(async () => {
|
|
261
233
|
try {
|
|
262
234
|
await tasksClient.deleteTask({ name });
|
|
263
235
|
deletedCount++;
|
|
264
236
|
} catch (e) {
|
|
265
|
-
|
|
266
|
-
if (e.code !== 5) {
|
|
267
|
-
console.warn(`[Planner] Failed to delete ${name}: ${e.message}`);
|
|
268
|
-
}
|
|
237
|
+
if (e.code !== 5) console.warn(`[Planner] Failed to delete ${name}: ${e.message}`);
|
|
269
238
|
}
|
|
270
239
|
})));
|
|
271
240
|
|
|
272
241
|
} catch (e) {
|
|
273
242
|
console.error(`[Planner] GC Error: ${e.message}`);
|
|
274
243
|
}
|
|
275
|
-
|
|
276
244
|
return deletedCount;
|
|
277
245
|
}
|
|
278
246
|
|
|
@@ -286,10 +254,21 @@ function shouldRunOnDate(schedule, dateObj) {
|
|
|
286
254
|
return true;
|
|
287
255
|
}
|
|
288
256
|
|
|
289
|
-
function
|
|
257
|
+
function getNextRunWindow(schedule, targetDateObj) {
|
|
290
258
|
const [h, m] = (schedule.time || '02:00').split(':').map(Number);
|
|
291
|
-
|
|
259
|
+
let runTime = new Date(targetDateObj);
|
|
292
260
|
runTime.setUTCHours(h, m, 0, 0);
|
|
261
|
+
|
|
262
|
+
const now = Date.now();
|
|
263
|
+
// If idealized time is in the past, shift to NEXT available window relative to NOW
|
|
264
|
+
if (runTime.getTime() < now) {
|
|
265
|
+
const nextWindow = new Date();
|
|
266
|
+
nextWindow.setUTCHours(h, m, 0, 0);
|
|
267
|
+
if (nextWindow.getTime() <= now) {
|
|
268
|
+
nextWindow.setUTCDate(nextWindow.getUTCDate() + 1);
|
|
269
|
+
}
|
|
270
|
+
return nextWindow.getTime() / 1000;
|
|
271
|
+
}
|
|
293
272
|
return runTime.getTime() / 1000;
|
|
294
273
|
}
|
|
295
274
|
|
|
@@ -340,10 +319,13 @@ async function dispatchTasks(tasks) {
|
|
|
340
319
|
|
|
341
320
|
} catch (e) {
|
|
342
321
|
// ALREADY_EXISTS (6) or CONFLICT (409)
|
|
343
|
-
// This happens if we try to recreate a task that was recently deleted (Tombstone)
|
|
344
|
-
// or if it genuinely already exists.
|
|
345
322
|
if (e.code === 6 || e.code === 409) {
|
|
346
|
-
|
|
323
|
+
// ENHANCED LOGGING:
|
|
324
|
+
const eta = t.runAtSeconds
|
|
325
|
+
? new Date(t.runAtSeconds * 1000).toISOString()
|
|
326
|
+
: 'Immediate';
|
|
327
|
+
|
|
328
|
+
console.warn(`[Planner] ⚠️ Task collision (Tombstone/Exists): ${t.computation} | Target: ${t.targetDate} | Planned ETA: ${eta} | Hash: ${t.configHash}`);
|
|
347
329
|
return { status: 'exists' };
|
|
348
330
|
}
|
|
349
331
|
|