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: Smart Reconciliation & Tombstone Aware
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; // Look back to ensure recent history is correct
21
- const PLANNING_LOOKAHEAD_HOURS = 24; // Schedule future tasks
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(); // Use Map to deduplicate by Task Name
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: getRunTimeSeconds(root.schedule, dateObj),
118
+ runAtSeconds: runAt,
123
119
  configHash: root.hash,
124
120
  queuePath: getQueuePath(),
125
- reason: `TRIGGERED_BY_${entry.name}_${reason}` // Track what triggered this root
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. Handle ROOT Tasks: root-{kebabName}-{date}-{hash}
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. Handle RECOVERY Tasks: recovery-{kebabName}-{date}-{timestamp}
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
- // 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
- }
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 getRunTimeSeconds(schedule, dateObj) {
257
+ function getNextRunWindow(schedule, targetDateObj) {
290
258
  const [h, m] = (schedule.time || '02:00').split(':').map(Number);
291
- const runTime = new Date(dateObj);
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
- console.warn(`[Planner] Task skipped (Already Exists/Tombstone): ${t.computation} @ ${t.targetDate}`);
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.776",
3
+ "version": "1.0.778",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [