bulltrackers-module 1.0.776 → 1.0.777
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.3: Smart Reconciliation & Future-Aligned Backfills
|
|
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
|
+
* * * UPDATE: Backfills now respect the 'next available window' instead of running instantly.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
const { CloudTasksClient } = require('@google-cloud/tasks');
|
|
@@ -62,7 +63,6 @@ async function planComputations(req, res) {
|
|
|
62
63
|
console.log(`[Planner] Reconciling window: ${windowStart.toISOString()} to ${windowEnd.toISOString()}`);
|
|
63
64
|
|
|
64
65
|
// 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
66
|
const manifestMap = new Map(manifest.map(m => [m.name, m]));
|
|
67
67
|
const getRoots = (entry, visited = new Set()) => {
|
|
68
68
|
if (visited.has(entry.name)) return [];
|
|
@@ -112,17 +112,19 @@ async function planComputations(req, res) {
|
|
|
112
112
|
|
|
113
113
|
roots.forEach(root => {
|
|
114
114
|
// Unique Task Key: RootName + Date + Hash
|
|
115
|
-
// Including Hash in key ensures we don't dedupe against a DIFFERENT version
|
|
116
115
|
const taskKey = `root-${toKebab(root.originalName)}-${dateStr}-${root.hash}`;
|
|
117
116
|
|
|
118
117
|
if (!tasksToSchedule.has(taskKey)) {
|
|
118
|
+
// NEW: Calculate the NEXT valid run window instead of using the past date
|
|
119
|
+
const runAt = getNextRunWindow(root.schedule, dateObj);
|
|
120
|
+
|
|
119
121
|
tasksToSchedule.set(taskKey, {
|
|
120
122
|
computation: root.originalName,
|
|
121
123
|
targetDate: dateStr,
|
|
122
|
-
runAtSeconds:
|
|
124
|
+
runAtSeconds: runAt,
|
|
123
125
|
configHash: root.hash,
|
|
124
126
|
queuePath: getQueuePath(),
|
|
125
|
-
reason: `TRIGGERED_BY_${entry.name}_${reason}`
|
|
127
|
+
reason: `TRIGGERED_BY_${entry.name}_${reason}`
|
|
126
128
|
});
|
|
127
129
|
}
|
|
128
130
|
});
|
|
@@ -164,7 +166,6 @@ async function planComputations(req, res) {
|
|
|
164
166
|
|
|
165
167
|
/**
|
|
166
168
|
* ENTRY POINT 2: The Watchdog
|
|
167
|
-
* Trigger: Cloud Scheduler -> "*\/15 * * * *" (Every 15 mins)
|
|
168
169
|
*/
|
|
169
170
|
async function runWatchdog(req, res) {
|
|
170
171
|
try {
|
|
@@ -204,75 +205,48 @@ async function runWatchdog(req, res) {
|
|
|
204
205
|
|
|
205
206
|
async function cleanupOrphanedTasks() {
|
|
206
207
|
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
|
-
|
|
208
|
+
const activeComputations = new Map(manifest.map(m => [toKebab(m.originalName), m.hash]));
|
|
213
209
|
const limit = pLimit(CLOUD_TASKS_CONCURRENCY);
|
|
214
210
|
let deletedCount = 0;
|
|
215
211
|
|
|
216
212
|
try {
|
|
217
213
|
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
|
-
})) {
|
|
214
|
+
for await (const task of tasksClient.listTasksAsync({ parent, responseView: 'BASIC', pageSize: 1000 })) {
|
|
225
215
|
const taskNameFull = task.name;
|
|
226
216
|
const taskNameShort = taskNameFull.split('/').pop();
|
|
227
217
|
|
|
228
|
-
// 1.
|
|
218
|
+
// 1. Root Tasks
|
|
229
219
|
const rootMatch = taskNameShort.match(/^root-(.+)-\d{4}-\d{2}-\d{2}-(.+)$/);
|
|
230
|
-
|
|
231
220
|
if (rootMatch) {
|
|
232
221
|
const [_, kebabName, taskHash] = rootMatch;
|
|
233
222
|
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
|
-
}
|
|
223
|
+
if (!activeHash || activeHash !== taskHash) tasksToDelete.push(taskNameFull);
|
|
241
224
|
continue;
|
|
242
225
|
}
|
|
243
226
|
|
|
244
|
-
// 2.
|
|
227
|
+
// 2. Recovery Tasks
|
|
245
228
|
const recoveryMatch = taskNameShort.match(/^recovery-(.+)-\d{4}-\d{2}-\d{2}-/);
|
|
246
|
-
|
|
247
229
|
if (recoveryMatch) {
|
|
248
230
|
const [_, kebabName] = recoveryMatch;
|
|
249
|
-
if (!activeComputations.has(kebabName))
|
|
250
|
-
tasksToDelete.push(taskNameFull);
|
|
251
|
-
}
|
|
231
|
+
if (!activeComputations.has(kebabName)) tasksToDelete.push(taskNameFull);
|
|
252
232
|
}
|
|
253
233
|
}
|
|
254
234
|
|
|
255
235
|
if (tasksToDelete.length === 0) return 0;
|
|
256
|
-
|
|
257
236
|
console.log(`[Planner] 🗑️ Found ${tasksToDelete.length} stale/orphaned tasks. Deleting...`);
|
|
258
237
|
|
|
259
|
-
// 3. Delete in parallel
|
|
260
238
|
await Promise.all(tasksToDelete.map(name => limit(async () => {
|
|
261
239
|
try {
|
|
262
240
|
await tasksClient.deleteTask({ name });
|
|
263
241
|
deletedCount++;
|
|
264
242
|
} catch (e) {
|
|
265
|
-
|
|
266
|
-
if (e.code !== 5) {
|
|
267
|
-
console.warn(`[Planner] Failed to delete ${name}: ${e.message}`);
|
|
268
|
-
}
|
|
243
|
+
if (e.code !== 5) console.warn(`[Planner] Failed to delete ${name}: ${e.message}`);
|
|
269
244
|
}
|
|
270
245
|
})));
|
|
271
246
|
|
|
272
247
|
} catch (e) {
|
|
273
248
|
console.error(`[Planner] GC Error: ${e.message}`);
|
|
274
249
|
}
|
|
275
|
-
|
|
276
250
|
return deletedCount;
|
|
277
251
|
}
|
|
278
252
|
|
|
@@ -286,10 +260,31 @@ function shouldRunOnDate(schedule, dateObj) {
|
|
|
286
260
|
return true;
|
|
287
261
|
}
|
|
288
262
|
|
|
289
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Calculates the run time.
|
|
265
|
+
* If the Target Date's schedule is in the PAST, it projects execution to the NEXT available window (Today or Tomorrow).
|
|
266
|
+
*/
|
|
267
|
+
function getNextRunWindow(schedule, targetDateObj) {
|
|
290
268
|
const [h, m] = (schedule.time || '02:00').split(':').map(Number);
|
|
291
|
-
|
|
269
|
+
|
|
270
|
+
// 1. Calculate the ideal run time on the TARGET date
|
|
271
|
+
let runTime = new Date(targetDateObj);
|
|
292
272
|
runTime.setUTCHours(h, m, 0, 0);
|
|
273
|
+
|
|
274
|
+
const now = Date.now();
|
|
275
|
+
|
|
276
|
+
// 2. If ideal time is in the past, move it to the *next* occurrence relative to NOW
|
|
277
|
+
if (runTime.getTime() < now) {
|
|
278
|
+
const nextWindow = new Date();
|
|
279
|
+
nextWindow.setUTCHours(h, m, 0, 0);
|
|
280
|
+
|
|
281
|
+
// If today's window has passed, move to tomorrow
|
|
282
|
+
if (nextWindow.getTime() <= now) {
|
|
283
|
+
nextWindow.setUTCDate(nextWindow.getUTCDate() + 1);
|
|
284
|
+
}
|
|
285
|
+
return nextWindow.getTime() / 1000;
|
|
286
|
+
}
|
|
287
|
+
|
|
293
288
|
return runTime.getTime() / 1000;
|
|
294
289
|
}
|
|
295
290
|
|
|
@@ -339,14 +334,10 @@ async function dispatchTasks(tasks) {
|
|
|
339
334
|
return { status: 'scheduled' };
|
|
340
335
|
|
|
341
336
|
} catch (e) {
|
|
342
|
-
// 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
337
|
if (e.code === 6 || e.code === 409) {
|
|
346
|
-
console.warn(`[Planner] Task
|
|
338
|
+
console.warn(`[Planner] Task name collision (Tombstone?): ${t.computation} @ ${t.targetDate}`);
|
|
347
339
|
return { status: 'exists' };
|
|
348
340
|
}
|
|
349
|
-
|
|
350
341
|
console.error(`[Planner] Failed task ${t.computation}: ${e.message}`);
|
|
351
342
|
return { status: 'error' };
|
|
352
343
|
}
|