bulltrackers-module 1.0.642 → 1.0.644
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.
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* NEW: Added 'getAvailabilityWindow' for efficient batch availability lookups using range queries.
|
|
6
6
|
*/
|
|
7
7
|
const { normalizeName } = require('../utils/utils');
|
|
8
|
+
const { FieldPath } = require('@google-cloud/firestore');
|
|
8
9
|
|
|
9
10
|
const INDEX_COLLECTION = process.env.ROOT_DATA_AVAILABILITY_COLLECTION || 'system_root_data_index';
|
|
10
11
|
|
|
@@ -329,8 +330,8 @@ async function getAvailabilityWindow(deps, startDateStr, endDateStr) {
|
|
|
329
330
|
|
|
330
331
|
// Perform Range Query on Document ID (Date String)
|
|
331
332
|
const snapshot = await db.collection(INDEX_COLLECTION)
|
|
332
|
-
.where(
|
|
333
|
-
.where(
|
|
333
|
+
.where(FieldPath.documentId(), '>=', startDateStr)
|
|
334
|
+
.where(FieldPath.documentId(), '<=', endDateStr)
|
|
334
335
|
.get();
|
|
335
336
|
|
|
336
337
|
const availabilityMap = new Map();
|
|
@@ -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
|
-
//
|
|
127
|
-
|
|
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) {
|