bulltrackers-module 1.0.642 → 1.0.643

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.
@@ -22,6 +22,28 @@ try { calculationPackage = require('aiden-shared-calculations-unified'); } catch
22
22
  const calculations = calculationPackage.calculations;
23
23
 
24
24
  const MAX_RETRIES = 3;
25
+ const STALE_LOCK_THRESHOLD_MS = 1000 * 60 * 15; // 15 minutes
26
+
27
+ // =============================================================================
28
+ // HELPER: Firestore Timestamp Conversion
29
+ // =============================================================================
30
+ /**
31
+ * Converts a Firestore Timestamp or Date to milliseconds.
32
+ * Firestore stores Date objects as Timestamp objects, which have a .toDate() method.
33
+ * This function handles both cases correctly.
34
+ * @param {any} field - Firestore Timestamp, Date object, or string
35
+ * @returns {number} Milliseconds since epoch, or 0 if invalid
36
+ */
37
+ function getMillis(field) {
38
+ if (!field) return 0;
39
+ // Handle Firestore Timestamp (has .toDate() method)
40
+ if (field.toDate && typeof field.toDate === 'function') {
41
+ return field.toDate().getTime();
42
+ }
43
+ // Handle standard Date object or string
44
+ const date = new Date(field);
45
+ return isNaN(date.getTime()) ? 0 : date.getTime();
46
+ }
25
47
 
26
48
  // [NEW] Helper to push metric to Google Cloud
27
49
  async function pushMetric(type, value, labels) {
@@ -123,8 +145,20 @@ async function checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerI
123
145
  return { shouldRun: false, reason: 'Duplicate delivery: Task already IN_PROGRESS with same ID.' };
124
146
  }
125
147
  if (data.status === 'IN_PROGRESS' && !onDemand) {
126
- // On-demand can break locks if needed, but regular requests should wait
127
- return { shouldRun: false, reason: 'Collision: Task currently IN_PROGRESS by another worker.' };
148
+ // CRITICAL FIX: Check if the lock is stale before blocking
149
+ // If the task has been IN_PROGRESS for more than 15 minutes, break the lock
150
+ const lastActivityTime = getMillis(data.telemetry?.lastHeartbeat) || getMillis(data.startedAt);
151
+ const lockAge = Date.now() - lastActivityTime;
152
+
153
+ if (lockAge > STALE_LOCK_THRESHOLD_MS) {
154
+ // Stale lock detected - break it and allow this worker to claim it
155
+ // The transaction will overwrite the stale lock below
156
+ // Note: We can't log here because we're in a transaction, but the caller will log
157
+ // Continue to claim lease below
158
+ } else {
159
+ // Valid active lock - wait for it to complete
160
+ return { shouldRun: false, reason: 'Collision: Task currently IN_PROGRESS by another worker.' };
161
+ }
128
162
  }
129
163
  }
130
164
 
@@ -179,10 +213,29 @@ async function handleComputationTask(message, config, dependencies) {
179
213
  const db = dependencies.db;
180
214
 
181
215
  // --- STEP 1: IDEMPOTENCY CHECK (with on-demand override) ---
216
+ // Check for stale locks before the transaction (for logging purposes)
217
+ const preCheckSnap = await db.doc(ledgerPath).get();
218
+ let wasStaleLock = false;
219
+ if (preCheckSnap.exists) {
220
+ const preCheckData = preCheckSnap.data();
221
+ if (preCheckData.status === 'IN_PROGRESS' && preCheckData.workerId !== workerId) {
222
+ const lastActivityTime = getMillis(preCheckData.telemetry?.lastHeartbeat) || getMillis(preCheckData.startedAt);
223
+ const lockAge = Date.now() - lastActivityTime;
224
+ if (lockAge > STALE_LOCK_THRESHOLD_MS) {
225
+ wasStaleLock = true;
226
+ logger.log('WARN', `[Worker] 🧟 Breaking stale lock for ${computation}. Previous worker ${preCheckData.workerId} was inactive for ${Math.round(lockAge / 1000 / 60)} minutes.`);
227
+ }
228
+ }
229
+ }
230
+
182
231
  const gate = await checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerId, onDemand);
183
232
  if (!gate.shouldRun) {
184
233
  logger.log('WARN', `[Worker] 🛑 Idempotency Gate: Skipping ${computation}. Reason: ${gate.reason}`);
185
- return;
234
+ return;
235
+ }
236
+
237
+ if (wasStaleLock) {
238
+ logger.log('INFO', `[Worker] ✅ Successfully claimed lease for ${computation} after breaking stale lock.`);
186
239
  }
187
240
 
188
241
  if (onDemand) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.642",
3
+ "version": "1.0.643",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [