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.
@@ -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: 20,
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: parseInt(process.env.WORKER_CONCURRENCY || '100', 10),
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