bulltrackers-module 1.0.775 → 1.0.776
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
|
-
* @fileoverview Scheduler V3.
|
|
3
|
-
* * * 1. Reconcile:
|
|
4
|
-
* * 2. Purge:
|
|
2
|
+
* @fileoverview Scheduler V3.2: Smart Reconciliation & Tombstone Aware
|
|
3
|
+
* * * 1. Reconcile: Checks ENTIRE graph for stale hashes, triggers ROOTS if needed.
|
|
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
6
|
*/
|
|
7
7
|
|
|
@@ -41,8 +41,10 @@ async function initialize() {
|
|
|
41
41
|
|
|
42
42
|
console.log(`[Scheduler] Loaded ${manifest.length} computations.`);
|
|
43
43
|
}
|
|
44
|
+
|
|
44
45
|
/**
|
|
45
|
-
*
|
|
46
|
+
* ENTRY POINT 1: The Planner
|
|
47
|
+
* Trigger: Cloud Scheduler -> "0 * * * *" (Hourly)
|
|
46
48
|
*/
|
|
47
49
|
async function planComputations(req, res) {
|
|
48
50
|
try {
|
|
@@ -60,6 +62,7 @@ async function planComputations(req, res) {
|
|
|
60
62
|
console.log(`[Planner] Reconciling window: ${windowStart.toISOString()} to ${windowEnd.toISOString()}`);
|
|
61
63
|
|
|
62
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.
|
|
63
66
|
const manifestMap = new Map(manifest.map(m => [m.name, m]));
|
|
64
67
|
const getRoots = (entry, visited = new Set()) => {
|
|
65
68
|
if (visited.has(entry.name)) return [];
|
|
@@ -106,10 +109,12 @@ async function planComputations(req, res) {
|
|
|
106
109
|
if (reason) {
|
|
107
110
|
// If entry is stale, we must schedule its ROOT(s) to trigger the chain
|
|
108
111
|
const roots = getRoots(entry);
|
|
109
|
-
|
|
112
|
+
|
|
110
113
|
roots.forEach(root => {
|
|
114
|
+
// Unique Task Key: RootName + Date + Hash
|
|
115
|
+
// Including Hash in key ensures we don't dedupe against a DIFFERENT version
|
|
111
116
|
const taskKey = `root-${toKebab(root.originalName)}-${dateStr}-${root.hash}`;
|
|
112
|
-
|
|
117
|
+
|
|
113
118
|
if (!tasksToSchedule.has(taskKey)) {
|
|
114
119
|
tasksToSchedule.set(taskKey, {
|
|
115
120
|
computation: root.originalName,
|
|
@@ -126,24 +131,28 @@ async function planComputations(req, res) {
|
|
|
126
131
|
})));
|
|
127
132
|
|
|
128
133
|
// --- PHASE 2: GARBAGE COLLECTION ---
|
|
129
|
-
// (Keep your existing GC logic here)
|
|
130
134
|
console.log('[Planner] Starting Garbage Collection...');
|
|
131
135
|
const deletedCount = await cleanupOrphanedTasks();
|
|
132
136
|
|
|
133
137
|
// --- PHASE 3: DISPATCH ---
|
|
134
138
|
const taskList = Array.from(tasksToSchedule.values());
|
|
135
139
|
let scheduledCount = 0;
|
|
140
|
+
let existsCount = 0;
|
|
141
|
+
|
|
136
142
|
if (taskList.length > 0) {
|
|
137
143
|
const results = await dispatchTasks(taskList);
|
|
138
144
|
scheduledCount = results.filter(r => r.status === 'scheduled').length;
|
|
145
|
+
existsCount = results.filter(r => r.status === 'exists').length;
|
|
139
146
|
}
|
|
140
147
|
|
|
141
|
-
console.log(`[Planner] Complete. Scheduled: ${scheduledCount}, Deleted Orphans: ${deletedCount}`);
|
|
142
|
-
|
|
148
|
+
console.log(`[Planner] Complete. Scheduled: ${scheduledCount} (Exists/Tombstoned: ${existsCount}), Deleted Orphans: ${deletedCount}`);
|
|
149
|
+
|
|
143
150
|
return res.status(200).json({
|
|
144
151
|
status: 'success',
|
|
145
152
|
window: `${PLANNING_LOOKBACK_DAYS}d back`,
|
|
146
153
|
scheduled: scheduledCount,
|
|
154
|
+
exists: existsCount,
|
|
155
|
+
deleted: deletedCount,
|
|
147
156
|
stats
|
|
148
157
|
});
|
|
149
158
|
|
|
@@ -188,13 +197,14 @@ async function runWatchdog(req, res) {
|
|
|
188
197
|
return res.status(500).json({ error: error.message });
|
|
189
198
|
}
|
|
190
199
|
}
|
|
200
|
+
|
|
191
201
|
// =============================================================================
|
|
192
202
|
// ACTIVE GARBAGE COLLECTION LOGIC
|
|
193
203
|
// =============================================================================
|
|
194
204
|
|
|
195
205
|
async function cleanupOrphanedTasks() {
|
|
196
206
|
const parent = getQueuePath();
|
|
197
|
-
|
|
207
|
+
|
|
198
208
|
// Create a map of { kebabName: activeHash } for O(1) lookups
|
|
199
209
|
const activeComputations = new Map(
|
|
200
210
|
manifest.map(m => [toKebab(m.originalName), m.hash])
|
|
@@ -206,18 +216,16 @@ async function cleanupOrphanedTasks() {
|
|
|
206
216
|
try {
|
|
207
217
|
const tasksToDelete = [];
|
|
208
218
|
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
parent,
|
|
219
|
+
// Increase pageSize to 1000 to minimize pagination calls/warnings
|
|
220
|
+
for await (const task of tasksClient.listTasksAsync({
|
|
221
|
+
parent,
|
|
213
222
|
responseView: 'BASIC',
|
|
214
|
-
pageSize: 1000
|
|
223
|
+
pageSize: 1000
|
|
215
224
|
})) {
|
|
216
225
|
const taskNameFull = task.name;
|
|
217
|
-
const taskNameShort = taskNameFull.split('/').pop();
|
|
226
|
+
const taskNameShort = taskNameFull.split('/').pop();
|
|
218
227
|
|
|
219
228
|
// 1. Handle ROOT Tasks: root-{kebabName}-{date}-{hash}
|
|
220
|
-
// We capture the name AND the hash at the end
|
|
221
229
|
const rootMatch = taskNameShort.match(/^root-(.+)-\d{4}-\d{2}-\d{2}-(.+)$/);
|
|
222
230
|
|
|
223
231
|
if (rootMatch) {
|
|
@@ -234,10 +242,8 @@ async function cleanupOrphanedTasks() {
|
|
|
234
242
|
}
|
|
235
243
|
|
|
236
244
|
// 2. Handle RECOVERY Tasks: recovery-{kebabName}-{date}-{timestamp}
|
|
237
|
-
// We only delete these if the computation is completely gone.
|
|
238
|
-
// (Timestamps won't match a config hash, so we just check existence)
|
|
239
245
|
const recoveryMatch = taskNameShort.match(/^recovery-(.+)-\d{4}-\d{2}-\d{2}-/);
|
|
240
|
-
|
|
246
|
+
|
|
241
247
|
if (recoveryMatch) {
|
|
242
248
|
const [_, kebabName] = recoveryMatch;
|
|
243
249
|
if (!activeComputations.has(kebabName)) {
|
|
@@ -257,7 +263,7 @@ async function cleanupOrphanedTasks() {
|
|
|
257
263
|
deletedCount++;
|
|
258
264
|
} catch (e) {
|
|
259
265
|
// Ignore "NOT_FOUND" errors in case of race conditions
|
|
260
|
-
if (e.code !== 5) {
|
|
266
|
+
if (e.code !== 5) {
|
|
261
267
|
console.warn(`[Planner] Failed to delete ${name}: ${e.message}`);
|
|
262
268
|
}
|
|
263
269
|
}
|
|
@@ -302,10 +308,10 @@ async function dispatchTasks(tasks) {
|
|
|
302
308
|
|
|
303
309
|
return Promise.all(tasks.map(t => limit(async () => {
|
|
304
310
|
try {
|
|
305
|
-
const name = t.isRecovery
|
|
311
|
+
const name = t.isRecovery
|
|
306
312
|
? `recovery-${toKebab(t.computation)}-${t.targetDate}-${Date.now()}`
|
|
307
313
|
: `root-${toKebab(t.computation)}-${t.targetDate}-${t.configHash}`;
|
|
308
|
-
|
|
314
|
+
|
|
309
315
|
const taskName = `${t.queuePath}/tasks/${name}`;
|
|
310
316
|
|
|
311
317
|
const payload = {
|
|
@@ -331,8 +337,16 @@ async function dispatchTasks(tasks) {
|
|
|
331
337
|
|
|
332
338
|
await tasksClient.createTask({ parent: t.queuePath, task });
|
|
333
339
|
return { status: 'scheduled' };
|
|
340
|
+
|
|
334
341
|
} catch (e) {
|
|
335
|
-
|
|
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
|
+
if (e.code === 6 || e.code === 409) {
|
|
346
|
+
console.warn(`[Planner] Task skipped (Already Exists/Tombstone): ${t.computation} @ ${t.targetDate}`);
|
|
347
|
+
return { status: 'exists' };
|
|
348
|
+
}
|
|
349
|
+
|
|
336
350
|
console.error(`[Planner] Failed task ${t.computation}: ${e.message}`);
|
|
337
351
|
return { status: 'error' };
|
|
338
352
|
}
|