bulltrackers-module 1.0.300 → 1.0.302

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.
@@ -114,7 +114,7 @@ class StandardExecutor {
114
114
 
115
115
  usersSinceLastFlush += chunkSize;
116
116
  const heapStats = v8.getHeapStatistics();
117
- if (usersSinceLastFlush >= 5000 || (heapStats.used_heap_size / heapStats.heap_size_limit) > 0.70) {
117
+ if (usersSinceLastFlush >= 500 || (heapStats.used_heap_size / heapStats.heap_size_limit) > 0.70) {
118
118
  const flushResult = await StandardExecutor.flushBuffer(state, dateStr, passName, config, deps, shardIndexMap, executionStats, 'INTERMEDIATE', true, !hasFlushed);
119
119
  hasFlushed = true;
120
120
  StandardExecutor.mergeReports(aggregatedSuccess, aggregatedFailures, flushResult);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * FILENAME: computation-system/helpers/computation_dispatcher.js
3
3
  * PURPOSE: "Smart Dispatcher" - Analyzes state, initializes Run Counters, and dispatches tasks.
4
- * UPDATED: FIXED DEADLOCK by separating concurrency limits for Dates vs Tasks.
4
+ * UPDATED: Optimized Forensics - Only runs on Retries (Attempt > 1) or Single-Day runs.
5
5
  */
6
6
 
7
7
  const { getExpectedDateStrings, getEarliestDataDates, normalizeName, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils.js');
@@ -59,7 +59,7 @@ async function checkCrashForensics(db, date, pass, computationName) {
59
59
  * @param {Object} config - System config (Injected with topics)
60
60
  * @param {Object} dependencies - { db, logger, ... }
61
61
  * @param {Array} computationManifest - List of calculations
62
- * @param {Object} reqBody - (Optional) HTTP Body containing 'callbackUrl' and 'date'
62
+ * @param {Object} reqBody - (Optional) HTTP Body containing 'callbackUrl', 'date', and 'attempt'
63
63
  */
64
64
  async function dispatchComputationPass(config, dependencies, computationManifest, reqBody = {}) {
65
65
  const { logger, db } = dependencies;
@@ -67,9 +67,10 @@ async function dispatchComputationPass(config, dependencies, computationManifest
67
67
  const passToRun = String(config.COMPUTATION_PASS_TO_RUN);
68
68
 
69
69
  // Extract Date and Callback from request body (pushed by Workflow)
70
- // NOTE: 'dateStr' acts as the "Target Date" (Ceiling), usually T-1.
71
70
  const dateStr = reqBody.date || config.date;
72
71
  const callbackUrl = reqBody.callbackUrl || null;
72
+ // [NEW] Get Attempt Count (Default to 1 if missing)
73
+ const attemptCount = reqBody.attempt ? parseInt(reqBody.attempt) : 1;
73
74
 
74
75
  if (!passToRun) { return logger.log('ERROR', '[Dispatcher] No pass defined (COMPUTATION_PASS_TO_RUN). Aborting.'); }
75
76
  if (!dateStr) { return logger.log('ERROR', '[Dispatcher] No date defined. Aborting.'); }
@@ -81,7 +82,7 @@ async function dispatchComputationPass(config, dependencies, computationManifest
81
82
 
82
83
  if (!calcsInThisPass.length) { return logger.log('WARN', `[Dispatcher] No calcs for Pass ${passToRun}. Exiting.`); }
83
84
 
84
- logger.log('INFO', `🚀 [Dispatcher] Smart-Dispatching PASS ${passToRun} (Target: ${dateStr})`);
85
+ logger.log('INFO', `🚀 [Dispatcher] Smart-Dispatching PASS ${passToRun} (Target: ${dateStr}) [Attempt ${attemptCount}]`);
85
86
 
86
87
  // -- DATE ANALYSIS LOGIC (FIXED: RANGE SCAN) --
87
88
 
@@ -93,7 +94,7 @@ async function dispatchComputationPass(config, dependencies, computationManifest
93
94
  // 2. Generate the full range of dates to check
94
95
  let allExpectedDates = getExpectedDateStrings(startDate, endDate);
95
96
 
96
- // Safety fallback: if range is invalid or empty, default to target date only
97
+ // Safety fallback
97
98
  if (!allExpectedDates || allExpectedDates.length === 0) {
98
99
  logger.log('WARN', `[Dispatcher] Date range calculation returned empty (Start: ${startDate.toISOString()} -> End: ${endDate.toISOString()}). Defaulting to single target date.`);
99
100
  allExpectedDates = [dateStr];
@@ -104,9 +105,23 @@ async function dispatchComputationPass(config, dependencies, computationManifest
104
105
  const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
105
106
  const tasksToDispatch = [];
106
107
 
107
- // [FIX] Separate concurrency limits to prevent DEADLOCK
108
- const dateLimit = pLimit(20); // Parallel Days
109
- const forensicsLimit = pLimit(50); // Parallel Checks per day
108
+ // [FIX] Separate concurrency limits
109
+ const dateLimit = pLimit(20);
110
+ const forensicsLimit = pLimit(50);
111
+
112
+ // [NEW] SMART FORENSICS TRIGGER
113
+ // 1. If scanning > 5 days (Backfill), SKIP (Too expensive).
114
+ // 2. If attempt == 1 (First Run), SKIP (Assume Standard).
115
+ // 3. Only run if Attempt > 1 AND Small Batch.
116
+ const isBulkBackfill = allExpectedDates.length > 5;
117
+ const shouldRunForensics = (attemptCount > 1) && !isBulkBackfill;
118
+
119
+ if (!shouldRunForensics) {
120
+ if (isBulkBackfill) logger.log('INFO', `[Dispatcher] ⏩ Bulk Backfill (${allExpectedDates.length} days). Skipping Forensics.`);
121
+ else logger.log('INFO', `[Dispatcher] ⏩ First Attempt. Skipping Forensics (Defaulting to Standard).`);
122
+ } else {
123
+ logger.log('WARN', `[Dispatcher] 🕵️‍♀️ Retry Detected (Attempt ${attemptCount}). Enabling Forensic Crash Analysis.`);
124
+ }
110
125
 
111
126
  const analysisPromises = allExpectedDates.map(d => dateLimit(async () => {
112
127
  try {
@@ -120,8 +135,6 @@ async function dispatchComputationPass(config, dependencies, computationManifest
120
135
  const prevDate = new Date(d + 'T00:00:00Z');
121
136
  prevDate.setUTCDate(prevDate.getUTCDate() - 1);
122
137
  prevDateStr = prevDate.toISOString().slice(0, 10);
123
-
124
- // Only fetch previous status if it's within valid range
125
138
  if (prevDate >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
126
139
  fetchPromises.push(fetchComputationStatus(prevDateStr, config, dependencies));
127
140
  }
@@ -138,7 +151,6 @@ async function dispatchComputationPass(config, dependencies, computationManifest
138
151
 
139
152
  const report = analyzeDateExecution(d, calcsInThisPass, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus);
140
153
 
141
- // Handle Status Updates (Impossible / Blocked)
142
154
  const statusUpdates = {};
143
155
  report.impossible.forEach(item => {
144
156
  if (dailyStatus[item.name]?.hash !== STATUS_IMPOSSIBLE) {
@@ -159,12 +171,14 @@ async function dispatchComputationPass(config, dependencies, computationManifest
159
171
 
160
172
  const validToRun = [...report.runnable, ...report.reRuns];
161
173
 
162
- // [FIX] Use separate 'forensicsLimit' here to avoid deadlock with 'dateLimit'
163
174
  await Promise.all(validToRun.map(item => forensicsLimit(async () => {
164
175
  const compName = normalizeName(item.name);
165
176
 
166
- // 1. Determine Resource Requirements
167
- const requiredResource = await checkCrashForensics(db, d, passToRun, compName);
177
+ // [UPDATED] Conditional Forensics
178
+ let requiredResource = 'standard';
179
+ if (shouldRunForensics) {
180
+ requiredResource = await checkCrashForensics(db, d, passToRun, compName);
181
+ }
168
182
 
169
183
  const uniqueDispatchId = crypto.randomUUID();
170
184
  tasksToDispatch.push({
@@ -178,11 +192,10 @@ async function dispatchComputationPass(config, dependencies, computationManifest
178
192
  triggerReason: item.reason || "Unknown",
179
193
  dependencyResultHashes: item.dependencyResultHashes || {},
180
194
  timestamp: Date.now(),
181
- resources: requiredResource // 'standard' or 'high-mem'
195
+ resources: requiredResource
182
196
  });
183
197
  })));
184
198
 
185
- // [PROGRESS LOG] This should now fire correctly
186
199
  logger.log('INFO', `[Dispatcher] Analyzed ${d}: ${validToRun.length} tasks (Cumulative: ${tasksToDispatch.length})`);
187
200
 
188
201
  } catch (e) {
@@ -196,14 +209,13 @@ async function dispatchComputationPass(config, dependencies, computationManifest
196
209
 
197
210
  if (tasksToDispatch.length > 0) {
198
211
 
199
- // 1. Initialize Shared State Document (The Counter)
200
212
  const runId = crypto.randomUUID();
201
213
  const metaStatePath = `computation_runs/${runId}`;
202
214
 
203
215
  if (callbackUrl) {
204
216
  await db.doc(metaStatePath).set({
205
217
  createdAt: new Date(),
206
- date: dateStr, // Acts as the "Job Label" (target date)
218
+ date: dateStr,
207
219
  pass: passToRun,
208
220
  totalTasks: tasksToDispatch.length,
209
221
  remainingTasks: tasksToDispatch.length,
@@ -213,29 +225,23 @@ async function dispatchComputationPass(config, dependencies, computationManifest
213
225
  logger.log('INFO', `[Dispatcher] 🏁 Run State Initialized: ${runId}. Tasks: ${tasksToDispatch.length}`);
214
226
  }
215
227
 
216
- // 2. Attach Run Metadata
217
228
  tasksToDispatch.forEach(task => {
218
229
  task.runId = runId;
219
230
  task.metaStatePath = callbackUrl ? metaStatePath : null;
220
231
  });
221
232
 
222
- // 3. Create Audit Ledger Entries
223
233
  const finalDispatched = [];
224
- const txnLimit = pLimit(50); // Increased txn throughput
234
+ const txnLimit = pLimit(50);
225
235
 
226
236
  const txnPromises = tasksToDispatch.map(task => txnLimit(async () => {
227
237
  const ledgerRef = db.collection(`computation_audit_ledger/${task.date}/passes/${task.pass}/tasks`).doc(task.computation);
228
-
229
238
  try {
230
239
  await db.runTransaction(async (t) => {
231
240
  const doc = await t.get(ledgerRef);
232
-
233
241
  if (doc.exists) {
234
242
  const data = doc.data();
235
- // Strict Idempotency: If completed, don't run again.
236
243
  if (data.status === 'COMPLETED') return false;
237
244
  }
238
-
239
245
  t.set(ledgerRef, {
240
246
  status: 'PENDING',
241
247
  dispatchId: task.dispatchId,
@@ -245,15 +251,12 @@ async function dispatchComputationPass(config, dependencies, computationManifest
245
251
  createdAt: new Date(),
246
252
  dispatcherHash: currentManifestHash,
247
253
  triggerReason: task.triggerReason,
248
- resources: task.resources, // Log intended resource type
254
+ resources: task.resources,
249
255
  retries: 0
250
256
  }, { merge: true });
251
-
252
257
  return true;
253
258
  });
254
-
255
259
  finalDispatched.push(task);
256
-
257
260
  } catch (txnErr) {
258
261
  logger.log('WARN', `[Dispatcher] Transaction failed for ${task.computation}: ${txnErr.message}`);
259
262
  }
@@ -261,13 +264,10 @@ async function dispatchComputationPass(config, dependencies, computationManifest
261
264
 
262
265
  await Promise.all(txnPromises);
263
266
 
264
- // 4. Publish to Pub/Sub (Segregated by Resources)
265
267
  if (finalDispatched.length > 0) {
266
-
267
268
  const standardTasks = finalDispatched.filter(t => t.resources !== 'high-mem');
268
269
  const highMemTasks = finalDispatched.filter(t => t.resources === 'high-mem');
269
270
 
270
- // Publish Standard
271
271
  if (standardTasks.length > 0) {
272
272
  logger.log('INFO', `[Dispatcher] ✅ Publishing ${standardTasks.length} Standard tasks...`);
273
273
  await pubsubUtils.batchPublishTasks(dependencies, {
@@ -278,7 +278,6 @@ async function dispatchComputationPass(config, dependencies, computationManifest
278
278
  });
279
279
  }
280
280
 
281
- // Publish High-Mem
282
281
  if (highMemTasks.length > 0) {
283
282
  logger.log('INFO', `[Dispatcher] 🏋️‍♀️ Publishing ${highMemTasks.length} tasks to HIGH-MEM infrastructure.`);
284
283
  await pubsubUtils.batchPublishTasks(dependencies, {
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * FILENAME: computation-system/helpers/computation_worker.js
3
3
  * PURPOSE: Consumes tasks, executes logic, and signals Workflow upon Batch Completion.
4
- * UPDATED: Implements IAM Auth for Workflow Callbacks.
5
- * UPDATED: Implements Peak Memory Heartbeat and Resource Tier tracking.
4
+ * UPDATED: Added "Contention-Aware Retry" for the Batch Counter to fix ABORTED errors.
6
5
  */
7
6
 
8
7
  const { executeDispatchTask } = require('../WorkflowOrchestrator.js');
@@ -99,24 +98,56 @@ async function triggerWorkflowCallback(url, status, logger) {
99
98
  }
100
99
 
101
100
  /**
102
- * Helper: Decrements 'remainingTasks' in Firestore.
101
+ * [UPDATED] Helper: Decrements 'remainingTasks' in Firestore.
102
+ * NOW INCLUDES CONTENTION RETRY LOGIC (The "Sentinel" Fix)
103
103
  */
104
104
  async function decrementAndCheck(db, metaStatePath, logger) {
105
105
  if (!metaStatePath) return null;
106
- try {
107
- const result = await db.runTransaction(async (t) => {
108
- const ref = db.doc(metaStatePath);
109
- const doc = await t.get(ref);
110
- if (!doc.exists) return null;
111
- const data = doc.data();
112
- const newRemaining = (data.remainingTasks || 0) - 1;
113
- t.update(ref, { remainingTasks: newRemaining, lastUpdated: new Date() });
114
- return { remaining: newRemaining, callbackUrl: data.callbackUrl };
115
- });
116
- if (result && result.remaining <= 0) return result.callbackUrl;
117
- } catch (e) {
118
- logger.log('ERROR', `[Worker] Failed to decrement batch counter: ${e.message}`);
106
+
107
+ const MAX_CONTENTION_RETRIES = 10;
108
+ let attempt = 0;
109
+
110
+ while (attempt < MAX_CONTENTION_RETRIES) {
111
+ try {
112
+ const result = await db.runTransaction(async (t) => {
113
+ const ref = db.doc(metaStatePath);
114
+ const doc = await t.get(ref);
115
+ if (!doc.exists) return null;
116
+
117
+ const data = doc.data();
118
+ // Safety: Don't decrement below zero
119
+ const currentRemaining = data.remainingTasks || 0;
120
+ if (currentRemaining <= 0) return { remaining: 0, callbackUrl: data.callbackUrl };
121
+
122
+ const newRemaining = currentRemaining - 1;
123
+ t.update(ref, { remainingTasks: newRemaining, lastUpdated: new Date() });
124
+
125
+ return { remaining: newRemaining, callbackUrl: data.callbackUrl };
126
+ });
127
+
128
+ // Success! Check if we are the "Sentinel" (the last one)
129
+ if (result && result.remaining <= 0) return result.callbackUrl;
130
+ return null; // We decremented successfully, but weren't the last one.
131
+
132
+ } catch (e) {
133
+ // Check if it's a contention error (ABORTED/10 or DEADLINE_EXCEEDED/4)
134
+ const isContention = e.code === 10 || e.code === 4 || (e.message && e.message.includes('contention'));
135
+
136
+ if (isContention) {
137
+ attempt++;
138
+ // JITTER: Random delay between 50ms and 500ms to desynchronize the herd
139
+ const delay = Math.floor(Math.random() * 450) + 50;
140
+ logger.log('WARN', `[Worker] Batch counter contention (Attempt ${attempt}/${MAX_CONTENTION_RETRIES}). Retrying in ${delay}ms...`);
141
+ await new Promise(r => setTimeout(r, delay));
142
+ } else {
143
+ // Fatal error (permission, etc)
144
+ logger.log('ERROR', `[Worker] Fatal error decrementing batch counter: ${e.message}`);
145
+ return null;
146
+ }
147
+ }
119
148
  }
149
+
150
+ logger.log('ERROR', `[Worker] Failed to decrement batch counter after ${MAX_CONTENTION_RETRIES} attempts. The count will be inaccurate.`);
120
151
  return null;
121
152
  }
122
153
 
@@ -48,7 +48,7 @@ async function loadDataByRefs(config, deps, refs) {
48
48
  const { withRetry } = calculationUtils;
49
49
  if (!refs || !refs.length) return {};
50
50
  const mergedPortfolios = {};
51
- const batchSize = config.partRefBatchSize || 50;
51
+ const batchSize = config.partRefBatchSize || 10;
52
52
  for (let i = 0; i < refs.length; i += batchSize) {
53
53
  const batchRefs = refs.slice(i, i + batchSize);
54
54
  const snapshots = await withRetry(() => db.getAll(...batchRefs), `getAll(batch ${Math.floor(i / batchSize)})`);
@@ -145,7 +145,7 @@ async function* streamPortfolioData(config, deps, dateString, providedRefs = nul
145
145
  const { logger } = deps;
146
146
  const refs = providedRefs || (await getPortfolioPartRefs(config, deps, dateString));
147
147
  if (refs.length === 0) { logger.log('WARN', `[streamPortfolioData] No portfolio refs found for ${dateString}. Stream is empty.`); return; }
148
- const batchSize = config.partRefBatchSize || 50;
148
+ const batchSize = config.partRefBatchSize || 10;
149
149
  logger.log('INFO', `[streamPortfolioData] Streaming ${refs.length} portfolio parts in chunks of ${batchSize}...`);
150
150
  for (let i = 0; i < refs.length; i += batchSize) {
151
151
  const batchRefs = refs.slice(i, i + batchSize);
@@ -160,7 +160,7 @@ async function* streamHistoryData(config, deps, dateString, providedRefs = null)
160
160
  const { logger } = deps;
161
161
  const refs = providedRefs || (await getHistoryPartRefs(config, deps, dateString));
162
162
  if (refs.length === 0) { logger.log('WARN', `[streamHistoryData] No history refs found for ${dateString}. Stream is empty.`); return; }
163
- const batchSize = config.partRefBatchSize || 50;
163
+ const batchSize = config.partRefBatchSize || 10;
164
164
  logger.log('INFO', `[streamHistoryData] Streaming ${refs.length} history parts in chunks of ${batchSize}...`);
165
165
  for (let i = 0; i < refs.length; i += batchSize) {
166
166
  const batchRefs = refs.slice(i, i + batchSize);
@@ -1,6 +1,6 @@
1
1
  # Cloud Workflows Definition for BullTrackers Computation Pipeline
2
2
  # Orchestrates 5 sequential passes using Event-Driven Callbacks (Zero Polling).
3
- # FIXED: Restored 'passes' and 'max_retries' variables in init step.
3
+ # UPDATED: Passes 'attempt' count to Dispatcher to trigger Smart Forensics on retries.
4
4
 
5
5
  main:
6
6
  params: [input]
@@ -16,7 +16,7 @@ main:
16
16
  - yesterday_str: ${text.substring(time.format(yesterday_timestamp), 0, 10)}
17
17
  - date_to_run: ${default(map.get(input, "date"), yesterday_str)}
18
18
 
19
- # Configuration Variables (Restored)
19
+ # Configuration Variables
20
20
  - passes: ["1", "2", "3", "4", "5"]
21
21
  - max_retries: 3
22
22
 
@@ -71,6 +71,7 @@ main:
71
71
  body:
72
72
  date: ${date_to_run}
73
73
  callbackUrl: ${callback_url}
74
+ attempt: ${attempt_count} # [UPDATED] Critical for Smart Forensics logic
74
75
  auth:
75
76
  type: OIDC
76
77
  timeout: 1800 # 30 mins max for dispatch analysis
@@ -96,7 +97,7 @@ main:
96
97
  call: events.await_callback
97
98
  args:
98
99
  callback: ${callback_details}
99
- timeout: 10800 # UPDATED: Reduced from 86400 (24h) to 10800 (3h) to detect crashes faster
100
+ timeout: 10800 # Reduced to 3h to fail faster if stalled
100
101
  result: callback_request
101
102
 
102
103
  # 5. PROCESS SIGNAL
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.300",
3
+ "version": "1.0.302",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [