bulltrackers-module 1.0.737 → 1.0.739
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.
- package/functions/computation-system-v2/docs/admin.md +91 -0
- package/functions/computation-system-v2/docs/plans.md +588 -0
- package/functions/computation-system-v2/framework/core/Manifest.js +25 -1
- package/functions/computation-system-v2/framework/data/DataFetcher.js +34 -2
- package/functions/computation-system-v2/framework/data/SchemaRegistry.js +31 -7
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +50 -15
- package/functions/computation-system-v2/framework/execution/RemoteTaskRunner.js +74 -13
- package/functions/computation-system-v2/handlers/adminTest.js +327 -0
- package/functions/computation-system-v2/handlers/index.js +4 -0
- package/functions/computation-system-v2/index.js +15 -1
- package/package.json +1 -1
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Admin Test Endpoint for Computation System
|
|
3
|
+
*
|
|
4
|
+
* SECURITY: This endpoint is protected by GCP IAM (requireAuth: true).
|
|
5
|
+
* Only service accounts and users with cloudfunctions.invoker can access it.
|
|
6
|
+
*
|
|
7
|
+
* PURPOSE:
|
|
8
|
+
* - Test computations in production without waiting for schedule
|
|
9
|
+
* - Force re-runs of computations (bypass hash checks)
|
|
10
|
+
* - Test worker pool functionality
|
|
11
|
+
* - Run on specific entities for debugging
|
|
12
|
+
*
|
|
13
|
+
* USAGE:
|
|
14
|
+
* curl -X POST https://REGION-PROJECT.cloudfunctions.net/compute-admin-test \
|
|
15
|
+
* -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
|
|
16
|
+
* -H "Content-Type: application/json" \
|
|
17
|
+
* -d '{"action": "run", "computation": "UserPortfolioSummary", "date": "2026-01-25"}'
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const system = require('../index');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Admin test handler.
|
|
24
|
+
*/
|
|
25
|
+
async function adminTestHandler(req, res) {
|
|
26
|
+
const startTime = Date.now();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const {
|
|
30
|
+
action = 'status',
|
|
31
|
+
computation,
|
|
32
|
+
date = new Date().toISOString().split('T')[0],
|
|
33
|
+
entityIds,
|
|
34
|
+
limit = 10,
|
|
35
|
+
force = true, // Default to force for testing
|
|
36
|
+
useWorkerPool, // Override: true/false/undefined (use config)
|
|
37
|
+
dryRun = false
|
|
38
|
+
} = req.body || {};
|
|
39
|
+
|
|
40
|
+
console.log(`[AdminTest] Action: ${action}, Computation: ${computation}, Date: ${date}`);
|
|
41
|
+
|
|
42
|
+
switch (action) {
|
|
43
|
+
// =========================================================
|
|
44
|
+
// STATUS: Show system status and available computations
|
|
45
|
+
// =========================================================
|
|
46
|
+
case 'status': {
|
|
47
|
+
const manifest = await system.getManifest();
|
|
48
|
+
|
|
49
|
+
return res.status(200).json({
|
|
50
|
+
status: 'ok',
|
|
51
|
+
action: 'status',
|
|
52
|
+
systemInfo: {
|
|
53
|
+
computationCount: manifest.length,
|
|
54
|
+
computations: manifest.map(c => ({
|
|
55
|
+
name: c.originalName || c.name,
|
|
56
|
+
type: c.type,
|
|
57
|
+
pass: c.pass,
|
|
58
|
+
schedule: c.schedule
|
|
59
|
+
})),
|
|
60
|
+
workerPool: {
|
|
61
|
+
enabled: process.env.WORKER_POOL_ENABLED === 'true',
|
|
62
|
+
localMode: process.env.WORKER_LOCAL_MODE === 'true'
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
timestamp: new Date().toISOString()
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// =========================================================
|
|
70
|
+
// ANALYZE: Check what would run for a given date
|
|
71
|
+
// =========================================================
|
|
72
|
+
case 'analyze': {
|
|
73
|
+
const report = await system.analyze({ date });
|
|
74
|
+
|
|
75
|
+
return res.status(200).json({
|
|
76
|
+
status: 'ok',
|
|
77
|
+
action: 'analyze',
|
|
78
|
+
date,
|
|
79
|
+
report: {
|
|
80
|
+
runnable: report.runnable?.map(r => r.name || r) || [],
|
|
81
|
+
skipped: report.skipped?.map(r => ({ name: r.name, reason: r.reason })) || [],
|
|
82
|
+
blocked: report.blocked?.map(r => ({ name: r.name, reason: r.reason })) || [],
|
|
83
|
+
impossible: report.impossible?.map(r => ({ name: r.name, reason: r.reason })) || []
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// =========================================================
|
|
89
|
+
// RUN: Execute a single computation
|
|
90
|
+
// =========================================================
|
|
91
|
+
case 'run': {
|
|
92
|
+
if (!computation) {
|
|
93
|
+
return res.status(400).json({
|
|
94
|
+
status: 'error',
|
|
95
|
+
error: 'Missing "computation" field. Use action: "status" to list available computations.'
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Log worker pool override if specified
|
|
100
|
+
if (useWorkerPool !== undefined) {
|
|
101
|
+
console.log(`[AdminTest] Worker pool override: ${useWorkerPool ? 'ENABLED' : 'DISABLED'}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(`[AdminTest] Running ${computation} for ${date}...`);
|
|
105
|
+
console.log(`[AdminTest] Options: force=${force}, dryRun=${dryRun}, entityIds=${entityIds?.join(',') || 'all'}`);
|
|
106
|
+
|
|
107
|
+
const result = await system.runComputation({
|
|
108
|
+
date,
|
|
109
|
+
computation,
|
|
110
|
+
entityIds: entityIds || null,
|
|
111
|
+
dryRun,
|
|
112
|
+
force,
|
|
113
|
+
// Pass worker pool override explicitly (avoids env var caching issues)
|
|
114
|
+
useWorkerPool
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const duration = Date.now() - startTime;
|
|
118
|
+
|
|
119
|
+
return res.status(200).json({
|
|
120
|
+
status: 'ok',
|
|
121
|
+
action: 'run',
|
|
122
|
+
computation,
|
|
123
|
+
date,
|
|
124
|
+
result: {
|
|
125
|
+
status: result.status,
|
|
126
|
+
duration: result.duration,
|
|
127
|
+
resultCount: result.resultCount,
|
|
128
|
+
reason: result.reason,
|
|
129
|
+
hash: result.hash
|
|
130
|
+
},
|
|
131
|
+
totalDuration: duration,
|
|
132
|
+
workerPoolUsed: useWorkerPool ?? (process.env.WORKER_POOL_ENABLED === 'true')
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// =========================================================
|
|
137
|
+
// RUN_LIMITED: Run on a limited number of entities (safer)
|
|
138
|
+
// =========================================================
|
|
139
|
+
case 'run_limited': {
|
|
140
|
+
if (!computation) {
|
|
141
|
+
return res.status(400).json({
|
|
142
|
+
status: 'error',
|
|
143
|
+
error: 'Missing "computation" field'
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Get a sample of entities from BigQuery
|
|
148
|
+
const sampleEntities = await getSampleEntities(computation, date, limit);
|
|
149
|
+
|
|
150
|
+
if (!sampleEntities || sampleEntities.length === 0) {
|
|
151
|
+
return res.status(404).json({
|
|
152
|
+
status: 'error',
|
|
153
|
+
error: `No entities found for ${computation} on ${date}`
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(`[AdminTest] Running LIMITED test: ${sampleEntities.length} entities`);
|
|
158
|
+
|
|
159
|
+
const result = await system.runComputation({
|
|
160
|
+
date,
|
|
161
|
+
computation,
|
|
162
|
+
entityIds: sampleEntities,
|
|
163
|
+
dryRun,
|
|
164
|
+
force,
|
|
165
|
+
useWorkerPool // Pass worker pool override
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const duration = Date.now() - startTime;
|
|
169
|
+
|
|
170
|
+
return res.status(200).json({
|
|
171
|
+
status: 'ok',
|
|
172
|
+
action: 'run_limited',
|
|
173
|
+
computation,
|
|
174
|
+
date,
|
|
175
|
+
entitiesTested: sampleEntities,
|
|
176
|
+
result: {
|
|
177
|
+
status: result.status,
|
|
178
|
+
duration: result.duration,
|
|
179
|
+
resultCount: result.resultCount
|
|
180
|
+
},
|
|
181
|
+
totalDuration: duration
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// =========================================================
|
|
186
|
+
// TEST_WORKER: Direct test of worker function
|
|
187
|
+
// =========================================================
|
|
188
|
+
case 'test_worker': {
|
|
189
|
+
if (!computation || !entityIds || entityIds.length === 0) {
|
|
190
|
+
return res.status(400).json({
|
|
191
|
+
status: 'error',
|
|
192
|
+
error: 'Requires "computation" and "entityIds" array'
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Import worker's local execution function
|
|
197
|
+
const { executeLocal, loadComputation } = require('./worker');
|
|
198
|
+
|
|
199
|
+
// Verify computation exists
|
|
200
|
+
const CompClass = loadComputation(computation);
|
|
201
|
+
if (!CompClass) {
|
|
202
|
+
return res.status(400).json({
|
|
203
|
+
status: 'error',
|
|
204
|
+
error: `Unknown computation: ${computation}`
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Fetch real data for one entity
|
|
209
|
+
const config = require('../config/bulltrackers.config');
|
|
210
|
+
const { DataFetcher } = require('../framework/data/DataFetcher');
|
|
211
|
+
const { QueryBuilder } = require('../framework/data/QueryBuilder');
|
|
212
|
+
const { SchemaRegistry } = require('../framework/data/SchemaRegistry');
|
|
213
|
+
|
|
214
|
+
const schemaRegistry = new SchemaRegistry(config.bigquery, console);
|
|
215
|
+
const queryBuilder = new QueryBuilder(config.bigquery, schemaRegistry, console);
|
|
216
|
+
const dataFetcher = new DataFetcher(
|
|
217
|
+
{ ...config.bigquery, tables: config.tables },
|
|
218
|
+
queryBuilder,
|
|
219
|
+
console
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const compConfig = CompClass.getConfig();
|
|
223
|
+
const testEntityId = entityIds[0];
|
|
224
|
+
|
|
225
|
+
console.log(`[AdminTest] Fetching data for entity ${testEntityId}...`);
|
|
226
|
+
const data = await dataFetcher.fetchForComputation(compConfig.requires, date, [testEntityId]);
|
|
227
|
+
|
|
228
|
+
// Execute worker logic locally
|
|
229
|
+
console.log(`[AdminTest] Executing worker logic...`);
|
|
230
|
+
const workerResult = await executeLocal({
|
|
231
|
+
computationName: computation,
|
|
232
|
+
entityId: testEntityId,
|
|
233
|
+
date,
|
|
234
|
+
contextPackage: {
|
|
235
|
+
entityData: data,
|
|
236
|
+
references: {},
|
|
237
|
+
dependencies: {},
|
|
238
|
+
config: {}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const duration = Date.now() - startTime;
|
|
243
|
+
|
|
244
|
+
return res.status(200).json({
|
|
245
|
+
status: 'ok',
|
|
246
|
+
action: 'test_worker',
|
|
247
|
+
computation,
|
|
248
|
+
entityId: testEntityId,
|
|
249
|
+
date,
|
|
250
|
+
workerResult: workerResult.result,
|
|
251
|
+
duration
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
default:
|
|
256
|
+
return res.status(400).json({
|
|
257
|
+
status: 'error',
|
|
258
|
+
error: `Unknown action: ${action}`,
|
|
259
|
+
availableActions: ['status', 'analyze', 'run', 'run_limited', 'test_worker']
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error('[AdminTest] Error:', error);
|
|
265
|
+
return res.status(500).json({
|
|
266
|
+
status: 'error',
|
|
267
|
+
error: error.message,
|
|
268
|
+
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get a sample of entity IDs for testing
|
|
275
|
+
*/
|
|
276
|
+
async function getSampleEntities(computation, date, limit) {
|
|
277
|
+
try {
|
|
278
|
+
const { BigQuery } = require('@google-cloud/bigquery');
|
|
279
|
+
const config = require('../config/bulltrackers.config');
|
|
280
|
+
|
|
281
|
+
const bigquery = new BigQuery({
|
|
282
|
+
projectId: config.bigquery.projectId
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Load computation to get its config
|
|
286
|
+
const { loadComputation } = require('./worker');
|
|
287
|
+
const CompClass = loadComputation(computation);
|
|
288
|
+
|
|
289
|
+
if (!CompClass) return null;
|
|
290
|
+
|
|
291
|
+
const compConfig = CompClass.getConfig();
|
|
292
|
+
|
|
293
|
+
// Find the driver table (first table with entityField)
|
|
294
|
+
let driverTable = null;
|
|
295
|
+
let entityField = null;
|
|
296
|
+
|
|
297
|
+
for (const [tableName, tableSpec] of Object.entries(compConfig.requires || {})) {
|
|
298
|
+
const tableConfig = config.tables[tableName];
|
|
299
|
+
if (tableConfig?.entityField) {
|
|
300
|
+
driverTable = tableName;
|
|
301
|
+
entityField = tableConfig.entityField;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!driverTable) return null;
|
|
307
|
+
|
|
308
|
+
const query = `
|
|
309
|
+
SELECT DISTINCT ${entityField} as entity_id
|
|
310
|
+
FROM \`${config.bigquery.projectId}.${config.bigquery.dataset}.${driverTable}\`
|
|
311
|
+
WHERE date = @date
|
|
312
|
+
LIMIT @limit
|
|
313
|
+
`;
|
|
314
|
+
|
|
315
|
+
const [rows] = await bigquery.query({
|
|
316
|
+
query,
|
|
317
|
+
params: { date, limit }
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return rows.map(r => r.entity_id);
|
|
321
|
+
} catch (e) {
|
|
322
|
+
console.error('[AdminTest] Failed to get sample entities:', e);
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
module.exports = { adminTestHandler };
|
|
@@ -12,6 +12,7 @@ const { schedulerHandler } = require('./scheduler');
|
|
|
12
12
|
const { dispatcherHandler } = require('./dispatcher');
|
|
13
13
|
const { onDemandHandler } = require('./onDemand');
|
|
14
14
|
const { workerHandler, executeLocal } = require('./worker');
|
|
15
|
+
const { adminTestHandler } = require('./adminTest');
|
|
15
16
|
|
|
16
17
|
module.exports = {
|
|
17
18
|
// Unified scheduler - triggered every minute by Cloud Scheduler
|
|
@@ -27,6 +28,9 @@ module.exports = {
|
|
|
27
28
|
// Invoked by RemoteTaskRunner from Orchestrator
|
|
28
29
|
computationWorker: workerHandler,
|
|
29
30
|
|
|
31
|
+
// Admin test endpoint - for testing computations in production
|
|
32
|
+
computeAdminTest: adminTestHandler,
|
|
33
|
+
|
|
30
34
|
// For local testing
|
|
31
35
|
executeWorkerLocal: executeLocal
|
|
32
36
|
};
|
|
@@ -83,6 +83,16 @@ async function execute(options) {
|
|
|
83
83
|
/**
|
|
84
84
|
* WORKER ENTRY POINT: Run a single computation.
|
|
85
85
|
* (Used by Cloud Functions / Dispatcher)
|
|
86
|
+
*
|
|
87
|
+
* @param {Object} options
|
|
88
|
+
* @param {string} options.date - Target date (YYYY-MM-DD)
|
|
89
|
+
* @param {string} options.computation - Computation name
|
|
90
|
+
* @param {string[]} [options.entityIds] - Specific entities to run (null = all)
|
|
91
|
+
* @param {boolean} [options.dryRun] - If true, don't persist results
|
|
92
|
+
* @param {boolean} [options.force] - If true, bypass up-to-date checks
|
|
93
|
+
* @param {boolean} [options.useWorkerPool] - Override worker pool setting (undefined = use config)
|
|
94
|
+
* @param {Object} [options.config] - Override config
|
|
95
|
+
* @param {Object} [options.logger] - Custom logger
|
|
86
96
|
*/
|
|
87
97
|
async function runComputation(options) {
|
|
88
98
|
const {
|
|
@@ -90,6 +100,8 @@ async function runComputation(options) {
|
|
|
90
100
|
computation,
|
|
91
101
|
entityIds = null,
|
|
92
102
|
dryRun = false,
|
|
103
|
+
force = false,
|
|
104
|
+
useWorkerPool, // Runtime override for worker pool
|
|
93
105
|
config: customConfig = null,
|
|
94
106
|
logger = null
|
|
95
107
|
} = options;
|
|
@@ -112,7 +124,9 @@ async function runComputation(options) {
|
|
|
112
124
|
// This handles dependencies, data fetching, middleware, etc.
|
|
113
125
|
return orch.runSingle(entry, date, {
|
|
114
126
|
entityIds,
|
|
115
|
-
dryRun
|
|
127
|
+
dryRun,
|
|
128
|
+
force,
|
|
129
|
+
useWorkerPool // Pass override to Orchestrator
|
|
116
130
|
});
|
|
117
131
|
}
|
|
118
132
|
|