bulltrackers-module 1.0.736 → 1.0.738
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/config/bulltrackers.config.js +7 -3
- package/functions/computation-system-v2/docs/admin.md +91 -0
- package/functions/computation-system-v2/docs/architecture.md +59 -0
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +20 -0
- 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 +17 -1
- package/functions/computation-system-v2/test/test-full-pipeline.js +227 -0
- package/functions/computation-system-v2/test/test-worker-pool.js +208 -436
- package/package.json +1 -1
|
@@ -246,10 +246,12 @@ module.exports = {
|
|
|
246
246
|
execution: {
|
|
247
247
|
// Max concurrent entity processing (per-entity computations)
|
|
248
248
|
// Higher = faster but more memory. Tune based on your Cloud Function memory.
|
|
249
|
-
entityConcurrency:
|
|
249
|
+
entityConcurrency: 50,
|
|
250
250
|
|
|
251
251
|
// Batch size for BigQuery inserts
|
|
252
252
|
insertBatchSize: 500,
|
|
253
|
+
|
|
254
|
+
fetchBatchSize: 30000,
|
|
253
255
|
|
|
254
256
|
// Memory safety: max entities to load for a dependency
|
|
255
257
|
// If a dependency has more entities than this, use getDependency(name, entityId) instead
|
|
@@ -361,7 +363,7 @@ module.exports = {
|
|
|
361
363
|
// Max concurrent worker invocations
|
|
362
364
|
// Higher = faster but more network/GCS load
|
|
363
365
|
// Recommended: 100-200 for production
|
|
364
|
-
concurrency:
|
|
366
|
+
concurrency: 100,
|
|
365
367
|
|
|
366
368
|
// Worker invocation timeout (ms)
|
|
367
369
|
// Should be slightly less than worker function timeout
|
|
@@ -382,6 +384,8 @@ module.exports = {
|
|
|
382
384
|
// Useful for testing specific computations
|
|
383
385
|
forceOffloadComputations: process.env.WORKER_FORCE_COMPUTATIONS
|
|
384
386
|
? process.env.WORKER_FORCE_COMPUTATIONS.split(',')
|
|
385
|
-
: []
|
|
387
|
+
: [],
|
|
388
|
+
|
|
389
|
+
minEntitiesForOffload: 100,
|
|
386
390
|
}
|
|
387
391
|
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Admin Test Endpoint
|
|
2
|
+
|
|
3
|
+
## Deploy
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
node deploy.mjs ComputeAdminTest
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Usage Examples
|
|
10
|
+
|
|
11
|
+
### 1. Check System Status
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
TOKEN=$(gcloud auth print-identity-token --audiences="https://europe-west1-stocks-12345.cloudfunctions.net/compute-admin-test")
|
|
15
|
+
|
|
16
|
+
curl -X POST \
|
|
17
|
+
"https://europe-west1-stocks-12345.cloudfunctions.net/compute-admin-test" \
|
|
18
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
19
|
+
-H "Content-Type: application/json" \
|
|
20
|
+
-d '{"action": "status"}'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 2. Analyze What Would Run
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
curl -X POST \
|
|
27
|
+
"https://europe-west1-stocks-12345.cloudfunctions.net/compute-admin-test" \
|
|
28
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
29
|
+
-H "Content-Type: application/json" \
|
|
30
|
+
-d '{"action": "analyze", "date": "2026-01-25"}'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 3. Run Full Computation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
curl -X POST \
|
|
37
|
+
"https://europe-west1-stocks-12345.cloudfunctions.net/compute-admin-test" \
|
|
38
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
39
|
+
-H "Content-Type: application/json" \
|
|
40
|
+
-d '{"action": "run", "computation": "UserPortfolioSummary", "date": "2026-01-25", "force": true}'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 4. Run Limited Test
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
curl -X POST \
|
|
47
|
+
"https://europe-west1-stocks-12345.cloudfunctions.net/compute-admin-test" \
|
|
48
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
49
|
+
-H "Content-Type: application/json" \
|
|
50
|
+
-d '{"action": "run_limited", "computation": "UserPortfolioSummary", "date": "2026-01-25", "limit": 5}'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 5. Test Specific Entities
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
curl -X POST \
|
|
57
|
+
"https://europe-west1-stocks-12345.cloudfunctions.net/compute-admin-test" \
|
|
58
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
59
|
+
-H "Content-Type: application/json" \
|
|
60
|
+
-d '{"action": "run", "computation": "UserPortfolioSummary", "date": "2026-01-25", "entityIds": ["user-123", "user-456"], "force": true}'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 6. Test Worker Directly
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
curl -X POST \
|
|
67
|
+
"https://europe-west1-stocks-12345.cloudfunctions.net/compute-admin-test" \
|
|
68
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
69
|
+
-H "Content-Type: application/json" \
|
|
70
|
+
-d '{"action": "test_worker", "computation": "UserPortfolioSummary", "date": "2026-01-25", "entityIds": ["user-123"]}'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 7. Test with Worker Pool Override
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
curl -X POST \
|
|
77
|
+
"https://europe-west1-stocks-12345.cloudfunctions.net/compute-admin-test" \
|
|
78
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
79
|
+
-H "Content-Type: application/json" \
|
|
80
|
+
-d '{"action": "run", "computation": "UserPortfolioSummary", "date": "2026-01-25", "useWorkerPool": true, "force": true}'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Available Actions
|
|
84
|
+
|
|
85
|
+
| Action | Description |
|
|
86
|
+
|--------|-------------|
|
|
87
|
+
| `status` | List all computations and system status |
|
|
88
|
+
| `analyze` | Check what would run for a given date |
|
|
89
|
+
| `run` | Execute a full computation |
|
|
90
|
+
| `run_limited` | Execute on N random entities (safer for testing) |
|
|
91
|
+
| `test_worker` | Direct test of worker function logic |
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
```mermaid
|
|
2
|
+
graph TD
|
|
3
|
+
Root((System))
|
|
4
|
+
|
|
5
|
+
%% Subgraph: Scheduling & Control
|
|
6
|
+
subgraph Control_Plane [Control Plane]
|
|
7
|
+
Cron((Timer)) -->|Every Minute| Scheduler[Scheduler Handler]
|
|
8
|
+
Scheduler -->|Find Due & Zombies| StateRepo[(State DB)]
|
|
9
|
+
Scheduler -->|Dispatch Task| CloudTasks[Cloud Tasks Queue]
|
|
10
|
+
CloudTasks -->|HTTP POST w/ Backoff| Dispatcher[Dispatcher Handler]
|
|
11
|
+
Dispatcher -->|Run Computation| Orchestrator[Orchestrator]
|
|
12
|
+
Orchestrator -->|Return Status| Dispatcher
|
|
13
|
+
Dispatcher -.->|Blocked| Return503[503 Retry]
|
|
14
|
+
Return503 -.-> CloudTasks
|
|
15
|
+
Dispatcher -.->|Success / Skipped| Return200[200 OK]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
%% Subgraph: Execution
|
|
19
|
+
subgraph Execution_Core [Execution Core]
|
|
20
|
+
Orchestrator --> Manifest[Manifest Builder]
|
|
21
|
+
Orchestrator -->|Check Hashes & Deps| StateRepo
|
|
22
|
+
Orchestrator -->|Fetch Data| BigQuery[(BigQuery)]
|
|
23
|
+
Orchestrator --> ExecMode{Mode?}
|
|
24
|
+
|
|
25
|
+
ExecMode -->|Global / Light| LocalExec[Local Execution]
|
|
26
|
+
LocalExec --> Logic[Computation Logic]
|
|
27
|
+
Logic --> LocalExec
|
|
28
|
+
|
|
29
|
+
ExecMode -->|Per-Entity / Heavy| RemoteRunner[Remote Task Runner]
|
|
30
|
+
RemoteRunner -->|Upload Context| GCS[(Cloud Storage)]
|
|
31
|
+
RemoteRunner --> Worker[Worker Handler]
|
|
32
|
+
Worker -->|Download Context| GCS
|
|
33
|
+
Worker -->|Execute| Logic
|
|
34
|
+
Worker -->|Return Result| RemoteRunner
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
%% Subgraph: Persistence
|
|
38
|
+
subgraph Persistence [Persistence Layer]
|
|
39
|
+
LocalExec -->|Commit Results| StateRepo
|
|
40
|
+
RemoteRunner -->|Commit Batch| StateRepo
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
%% Single-root anchoring (critical)
|
|
44
|
+
Root --> Cron
|
|
45
|
+
Root -.-> Orchestrator
|
|
46
|
+
Root -.-> LocalExec
|
|
47
|
+
Root -.-> RemoteRunner
|
|
48
|
+
|
|
49
|
+
%% Styling
|
|
50
|
+
classDef plain fill:#ffffff,stroke:#333,stroke-width:1px;
|
|
51
|
+
classDef db fill:#e1f5fe,stroke:#01579b,stroke-width:2px;
|
|
52
|
+
classDef logic fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
|
|
53
|
+
classDef queue fill:#fff9c4,stroke:#fbc02d,stroke-width:2px;
|
|
54
|
+
|
|
55
|
+
class Cron,Scheduler,Dispatcher,Orchestrator,Manifest,LocalExec,RemoteRunner,Worker plain;
|
|
56
|
+
class StateRepo,BigQuery,GCS db;
|
|
57
|
+
class Logic logic;
|
|
58
|
+
class CloudTasks queue;
|
|
59
|
+
```
|
|
@@ -362,8 +362,28 @@ class Orchestrator {
|
|
|
362
362
|
|
|
363
363
|
/**
|
|
364
364
|
* Determine if a computation should use remote workers
|
|
365
|
+
*
|
|
366
|
+
* @param {Object} entry - Manifest entry
|
|
367
|
+
* @param {Object} options - Execution options
|
|
368
|
+
* @param {boolean} [options.useWorkerPool] - Runtime override (true/false/undefined)
|
|
369
|
+
* @param {boolean} [options.forceLocal] - Force local execution
|
|
365
370
|
*/
|
|
366
371
|
_shouldUseRemoteWorkers(entry, options) {
|
|
372
|
+
// Runtime override takes precedence (for admin testing)
|
|
373
|
+
// useWorkerPool: true -> force use worker pool
|
|
374
|
+
// useWorkerPool: false -> force local execution
|
|
375
|
+
// useWorkerPool: undefined -> use config
|
|
376
|
+
if (options.useWorkerPool === true) {
|
|
377
|
+
if (!this.remoteRunner) {
|
|
378
|
+
this._log('WARN', 'useWorkerPool=true but remoteRunner not initialized');
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
if (options.useWorkerPool === false) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
367
387
|
// No remote runner configured
|
|
368
388
|
if (!this.remoteRunner) return false;
|
|
369
389
|
|
|
@@ -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
|
};
|
|
@@ -19,10 +19,12 @@ const { ManifestBuilder } = require('./framework/core/Manifest');
|
|
|
19
19
|
const { Computation } = require('./framework/core/Computation');
|
|
20
20
|
|
|
21
21
|
// Add computations to config
|
|
22
|
+
// These are loaded from computation-system-v2/computations folder
|
|
22
23
|
config.computations = [
|
|
23
24
|
require('./computations/UserPortfolioSummary'),
|
|
24
25
|
require('./computations/PopularInvestorProfileMetrics'),
|
|
25
26
|
require('./computations/PopularInvestorRiskAssessment'),
|
|
27
|
+
require('./computations/PopularInvestorRiskMetrics'),
|
|
26
28
|
// Add more computations here as they're migrated
|
|
27
29
|
];
|
|
28
30
|
|
|
@@ -81,6 +83,16 @@ async function execute(options) {
|
|
|
81
83
|
/**
|
|
82
84
|
* WORKER ENTRY POINT: Run a single computation.
|
|
83
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
|
|
84
96
|
*/
|
|
85
97
|
async function runComputation(options) {
|
|
86
98
|
const {
|
|
@@ -88,6 +100,8 @@ async function runComputation(options) {
|
|
|
88
100
|
computation,
|
|
89
101
|
entityIds = null,
|
|
90
102
|
dryRun = false,
|
|
103
|
+
force = false,
|
|
104
|
+
useWorkerPool, // Runtime override for worker pool
|
|
91
105
|
config: customConfig = null,
|
|
92
106
|
logger = null
|
|
93
107
|
} = options;
|
|
@@ -110,7 +124,9 @@ async function runComputation(options) {
|
|
|
110
124
|
// This handles dependencies, data fetching, middleware, etc.
|
|
111
125
|
return orch.runSingle(entry, date, {
|
|
112
126
|
entityIds,
|
|
113
|
-
dryRun
|
|
127
|
+
dryRun,
|
|
128
|
+
force,
|
|
129
|
+
useWorkerPool // Pass override to Orchestrator
|
|
114
130
|
});
|
|
115
131
|
}
|
|
116
132
|
|