bulltrackers-module 1.0.290 → 1.0.291

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,14 +1,15 @@
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 "Last Worker" Callback Pattern.
4
+ * UPDATED: Implements IAM Auth for Workflow Callbacks.
5
5
  */
6
6
 
7
7
  const { executeDispatchTask } = require('../WorkflowOrchestrator.js');
8
8
  const { getManifest } = require('../topology/ManifestLoader');
9
9
  const { StructuredLogger } = require('../logger/logger');
10
10
  const { recordRunAttempt } = require('../persistence/RunRecorder');
11
- const https = require('https'); // [NEW] Required for callback
11
+ const https = require('https');
12
+ const { GoogleAuth } = require('google-auth-library'); // [NEW] Required for Auth
12
13
 
13
14
  let calculationPackage;
14
15
  try { calculationPackage = require('aiden-shared-calculations-unified');
@@ -19,40 +20,56 @@ const MAX_RETRIES = 3;
19
20
 
20
21
  /**
21
22
  * [NEW] Helper: Fires the webhook back to Google Cloud Workflows.
23
+ * UPDATED: Now generates an IAM Bearer Token to authenticate the request.
22
24
  */
23
- function triggerWorkflowCallback(url, status, logger) {
24
- if (!url) return Promise.resolve();
25
+ async function triggerWorkflowCallback(url, status, logger) {
26
+ if (!url) return;
25
27
  logger.log('INFO', `[Worker] 🔔 BATCH COMPLETE! Triggering Workflow Callback: ${status}`);
26
28
 
27
- return new Promise((resolve, reject) => {
28
- const body = JSON.stringify({
29
- status: status,
30
- timestamp: new Date().toISOString()
31
- });
32
-
33
- const req = https.request(url, {
34
- method: 'POST',
35
- headers: {
36
- 'Content-Type': 'application/json',
37
- 'Content-Length': Buffer.byteLength(body)
38
- }
39
- }, (res) => {
40
- if (res.statusCode >= 200 && res.statusCode < 300) {
41
- resolve();
42
- } else {
43
- logger.log('WARN', `Callback responded with ${res.statusCode}`);
44
- resolve(); // Don't crash the worker if callback fails, logic is done.
45
- }
29
+ try {
30
+ // 1. Get OAuth2 Access Token (Required for Workflows Callbacks)
31
+ const auth = new GoogleAuth({
32
+ scopes: ['https://www.googleapis.com/auth/cloud-platform']
46
33
  });
34
+ const client = await auth.getClient();
35
+ const accessToken = await client.getAccessToken();
36
+ const token = accessToken.token;
47
37
 
48
- req.on('error', (e) => {
49
- logger.log('ERROR', `Failed to trigger callback: ${e.message}`);
50
- resolve();
38
+ // 2. Send Authenticated Request
39
+ return new Promise((resolve, reject) => {
40
+ const body = JSON.stringify({
41
+ status: status,
42
+ timestamp: new Date().toISOString()
43
+ });
44
+
45
+ const req = https.request(url, {
46
+ method: 'POST',
47
+ headers: {
48
+ 'Content-Type': 'application/json',
49
+ 'Content-Length': Buffer.byteLength(body),
50
+ 'Authorization': `Bearer ${token}` // <--- CRITICAL FIX
51
+ }
52
+ }, (res) => {
53
+ if (res.statusCode >= 200 && res.statusCode < 300) {
54
+ resolve();
55
+ } else {
56
+ logger.log('WARN', `Callback responded with ${res.statusCode}`);
57
+ // We resolve anyway to avoid crashing the worker logic
58
+ resolve();
59
+ }
60
+ });
61
+
62
+ req.on('error', (e) => {
63
+ logger.log('ERROR', `Failed to trigger callback: ${e.message}`);
64
+ resolve();
65
+ });
66
+
67
+ req.write(body);
68
+ req.end();
51
69
  });
52
-
53
- req.write(body);
54
- req.end();
55
- });
70
+ } catch (e) {
71
+ logger.log('ERROR', `Failed to generate auth token for callback: ${e.message}`);
72
+ }
56
73
  }
57
74
 
58
75
  /**
@@ -133,12 +150,8 @@ async function handleComputationTask(message, config, dependencies) {
133
150
  failedAt: new Date()
134
151
  }, { merge: true });
135
152
 
136
- // [CRITICAL] Even if it failed, we MUST decrement the counter.
137
- // Otherwise the workflow waits 24h for a task that will never finish.
138
153
  const callbackUrl = await decrementAndCheck(db, metaStatePath, logger);
139
154
  if (callbackUrl) {
140
- // We signal SUCCESS to the workflow because the *Batch* is finished processing (even if this task failed).
141
- // The "monitor" or next pass can handle data gaps.
142
155
  await triggerWorkflowCallback(callbackUrl, 'SUCCESS', logger);
143
156
  }
144
157
 
@@ -162,7 +175,6 @@ async function handleComputationTask(message, config, dependencies) {
162
175
  try { computationManifest = getManifest(config.activeProductLines || [], calculations, runDependencies);
163
176
  } catch (manifestError) {
164
177
  logger.log('FATAL', `[Worker] Failed to load Manifest: ${manifestError.message}`);
165
- // Do NOT decrement here, let PubSub retry the task.
166
178
  return;
167
179
  }
168
180
 
@@ -178,7 +190,6 @@ async function handleComputationTask(message, config, dependencies) {
178
190
  const successUpdates = result?.updates?.successUpdates || {};
179
191
 
180
192
  if (failureReport.length > 0) {
181
- // Logic/Storage failure (handled internally by executor)
182
193
  const failReason = failureReport[0];
183
194
  throw new Error(failReason.error.message || 'Computation Logic Failed');
184
195
  }
@@ -189,7 +200,6 @@ async function handleComputationTask(message, config, dependencies) {
189
200
  logger.log('WARN', `[Worker] ⚠️ Empty Result: ${computation}`);
190
201
  }
191
202
 
192
- // 1. Mark Ledger as COMPLETED
193
203
  await db.collection(`computation_audit_ledger/${date}/passes/${pass}/tasks`).doc(computation).update({
194
204
  status: 'COMPLETED',
195
205
  completedAt: new Date()
@@ -197,18 +207,14 @@ async function handleComputationTask(message, config, dependencies) {
197
207
 
198
208
  await recordRunAttempt(db, { date, computation, pass }, 'SUCCESS', null, { durationMs: duration }, triggerReason);
199
209
 
200
- // 2. [NEW] Decrement Batch Counter & Check for Callback
210
+ // Decrement & Callback
201
211
  const callbackUrl = await decrementAndCheck(db, metaStatePath, logger);
202
-
203
- // 3. [NEW] If last one, fire callback
204
212
  if (callbackUrl) {
205
213
  await triggerWorkflowCallback(callbackUrl, 'SUCCESS', logger);
206
214
  }
207
215
  }
208
216
  } catch (err) {
209
217
  // --- ERROR HANDLING ---
210
-
211
- // Check for Permanent/Deterministic Errors
212
218
  const isDeterministicError = err.stage === 'SHARDING_LIMIT_EXCEEDED' ||
213
219
  err.stage === 'QUALITY_CIRCUIT_BREAKER' ||
214
220
  err.stage === 'SEMANTIC_GATE' ||
@@ -233,28 +239,23 @@ async function handleComputationTask(message, config, dependencies) {
233
239
 
234
240
  await recordRunAttempt(db, { date, computation, pass }, 'FAILURE', { message: err.message, stage: err.stage || 'PERMANENT_FAIL' }, { durationMs: 0 }, triggerReason);
235
241
 
236
- // [CRITICAL] Permanent failure -> Must decrement so workflow doesn't hang
237
242
  const callbackUrl = await decrementAndCheck(db, metaStatePath, logger);
238
243
  if (callbackUrl) {
239
244
  await triggerWorkflowCallback(callbackUrl, 'SUCCESS', logger);
240
245
  }
241
246
 
242
- return; // Do NOT throw, consume the message
247
+ return;
243
248
  } catch (dlqErr) { logger.log('FATAL', `[Worker] Failed to write to DLQ`, dlqErr); }
244
249
  }
245
250
 
246
- // Standard Retryable Error (Transient)
247
251
  if (retryCount >= MAX_RETRIES) {
248
- // Let the top-level poison check handle the decrement on the *next* delivery (or handle here if you prefer).
249
- // Standard practice: throw so PubSub handles the backoff and redelivery.
250
- // The poison logic at the top of this function will catch it on attempt N+1.
251
252
  throw err;
252
253
  }
253
254
 
254
255
  logger.log('ERROR', `[Worker] ❌ Crash: ${computation}: ${err.message}`);
255
256
  await recordRunAttempt(db, { date, computation, pass }, 'CRASH', { message: err.message, stack: err.stack, stage: 'SYSTEM_CRASH' }, { durationMs: 0 }, triggerReason);
256
257
 
257
- throw err; // Trigger Pub/Sub retry
258
+ throw err;
258
259
  }
259
260
  }
260
261
 
@@ -1,6 +1,7 @@
1
1
  # Cloud Workflows Definition for BullTrackers Computation Pipeline
2
2
  # Orchestrates 5 sequential passes using Event-Driven Callbacks (Zero Polling).
3
- # UPDATED: Implements "Callback Pattern" to eliminate arbitrary sleeps.
3
+ # FIXED: Replaced invalid 'sys' callback functions with 'events' library functions.
4
+ # FIXED: Proper extraction of 'callback_details.url' for the dispatcher.
4
5
 
5
6
  main:
6
7
  params: [input]
@@ -40,11 +41,17 @@ main:
40
41
  assign:
41
42
  - attempt_count: ${attempt_count + 1}
42
43
 
43
- # 1. GENERATE CALLBACK URL
44
- # This creates a unique HTTP endpoint that points specifically to this step execution.
44
+ # 1. GENERATE CALLBACK ENDPOINT
45
+ # We use the 'events' library. This returns an object containing the URL.
45
46
  - create_callback:
46
- call: sys.create_callback_url
47
- result: callback_url
47
+ call: events.create_callback_endpoint
48
+ args:
49
+ http_callback_method: "POST"
50
+ result: callback_details
51
+
52
+ - extract_callback_url:
53
+ assign:
54
+ - callback_url: ${callback_details.url}
48
55
 
49
56
  - log_start:
50
57
  call: sys.log
@@ -53,21 +60,20 @@ main:
53
60
  severity: "INFO"
54
61
 
55
62
  # 2. TRIGGER DISPATCHER
56
- # We pass the 'callback_url' to the dispatcher so it can hand it to the workers.
63
+ # We pass the extracted 'callback_url' string to the dispatcher.
57
64
  - trigger_dispatcher:
58
65
  call: http.post
59
66
  args:
60
67
  url: ${dispatcher_url}
61
68
  body:
62
69
  date: ${date_to_run}
63
- callbackUrl: ${callback_url} # <--- CRITICAL: Passing the token
70
+ callbackUrl: ${callback_url}
64
71
  auth:
65
72
  type: OIDC
66
73
  timeout: 1800 # 30 mins max for dispatch analysis
67
74
  result: dispatch_response
68
75
 
69
76
  # 3. CHECK FOR "NOTHING TO DO"
70
- # If the dispatcher found 0 tasks, it returns immediate success. We skip waiting.
71
77
  - check_immediate_completion:
72
78
  switch:
73
79
  - condition: ${dispatch_response.body.dispatched == 0}
@@ -83,19 +89,20 @@ main:
83
89
  next: pass_retry_loop
84
90
 
85
91
  # 4. WAIT FOR WORKER SIGNAL
86
- # The workflow freezes here (sleeps) until a worker hits the callback_url.
87
- # This eliminates the need for polling logic.
92
+ # We must pass the original 'callback_details' object here, not the URL string.
88
93
  - wait_for_completion:
89
- call: sys.await_callback
94
+ call: events.await_callback
90
95
  args:
91
- callback_url: ${callback_url}
92
- timeout: 86400 # Wait up to 24 hours for the batch to finish
96
+ callback: ${callback_details}
97
+ timeout: 86400 # Wait up to 24 hours
93
98
  result: callback_request
94
99
 
95
100
  # 5. PROCESS SIGNAL
96
- - evaluate_signal:
101
+ - parse_signal:
97
102
  assign:
98
103
  - signal_data: ${callback_request.http_request.body}
104
+
105
+ - evaluate_signal:
99
106
  switch:
100
107
  - condition: ${signal_data.status == "SUCCESS"}
101
108
  steps:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.290",
3
+ "version": "1.0.291",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -35,11 +35,12 @@
35
35
  "cors": "^2.8.5",
36
36
  "dotenv": "latest",
37
37
  "express": "^4.19.2",
38
+ "google-auth-library": "^10.5.0",
39
+ "graphviz": "latest",
38
40
  "node-graphviz": "^0.1.1",
39
41
  "p-limit": "^3.1.0",
40
42
  "require-all": "^3.0.0",
41
- "sharedsetup": "latest",
42
- "graphviz": "latest"
43
+ "sharedsetup": "latest"
43
44
  },
44
45
  "devDependencies": {
45
46
  "bulltracker-deployer": "file:../bulltracker-deployer"