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.1: Reconciler with Active Garbage Collection
3
- * * * 1. Reconcile: Iterates a time window (Past -> Future) to ensure valid tasks exist.
4
- * * 2. Purge: Scans the queue for "Orphans" (tasks for deleted computations) and deletes them.
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
- * REPLACEMENT for planComputations
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
- // Note: listTasksAsync handles pagination, but if you have thousands of tasks,
210
- // you might eventually need to handle page tokens explicitly if the library version is old.
211
- for await (const task of tasksClient.listTasksAsync({
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 // Increase page size to capture more per request
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
- if (e.code === 6 || e.code === 409) return { status: 'exists' };
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.775",
3
+ "version": "1.0.776",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [