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: Smart Reconciliation & Tombstone Aware
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: getRunTimeSeconds(root.schedule, dateObj),
124
+ runAtSeconds: runAt,
123
125
  configHash: root.hash,
124
126
  queuePath: getQueuePath(),
125
- reason: `TRIGGERED_BY_${entry.name}_${reason}` // Track what triggered this root
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. Handle ROOT Tasks: root-{kebabName}-{date}-{hash}
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. Handle RECOVERY Tasks: recovery-{kebabName}-{date}-{timestamp}
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
- // Ignore "NOT_FOUND" errors in case of race conditions
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
- function getRunTimeSeconds(schedule, dateObj) {
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
- const runTime = new Date(dateObj);
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 skipped (Already Exists/Tombstone): ${t.computation} @ ${t.targetDate}`);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.776",
3
+ "version": "1.0.777",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [