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
|
|
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');
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
//
|
|
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;
|
|
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;
|
|
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
|
-
#
|
|
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
|
|
44
|
-
#
|
|
44
|
+
# 1. GENERATE CALLBACK ENDPOINT
|
|
45
|
+
# We use the 'events' library. This returns an object containing the URL.
|
|
45
46
|
- create_callback:
|
|
46
|
-
call:
|
|
47
|
-
|
|
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
|
|
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}
|
|
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
|
-
#
|
|
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:
|
|
94
|
+
call: events.await_callback
|
|
90
95
|
args:
|
|
91
|
-
|
|
92
|
-
timeout: 86400 # Wait up to 24 hours
|
|
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
|
-
-
|
|
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.
|
|
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"
|