bulltrackers-module 1.0.807 → 1.0.809
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/alert-system-v3/handlers/ingest.js +2 -5
- package/functions/computation-system-v3/config/bulltrackers.config.js +4 -2
- package/functions/computation-system-v3/framework/adapters/StateRepository.js +24 -40
- package/functions/computation-system-v3/framework/core/Coordinator.js +17 -14
- package/functions/computation-system-v3/framework/scheduling/TaskScheduler.js +38 -20
- package/functions/computation-system-v3/framework/tests/unit/output/BehavioralAnomaly.json +4 -0
- package/functions/computation-system-v3/framework/tests/unit/output/GlobalAumPerAsset30D.json +3 -0
- package/functions/computation-system-v3/framework/tests/unit/output/NewSectorExposure.json +4 -0
- package/functions/computation-system-v3/framework/tests/unit/output/NewSocialPost.json +5 -0
- package/functions/computation-system-v3/framework/tests/unit/output/PIDailyAssetAUM.json +4 -0
- package/functions/computation-system-v3/framework/tests/unit/output/PiFeatureVectors.json +22 -0
- package/functions/computation-system-v3/framework/tests/unit/output/PiRecommender.json +4 -0
- package/functions/computation-system-v3/framework/tests/unit/output/PopularInvestorProfileMetrics.json +84 -0
- package/functions/computation-system-v3/framework/tests/unit/output/PositionInvestedIncrease.json +4 -0
- package/functions/computation-system-v3/framework/tests/unit/output/RiskScoreIncrease.json +4 -0
- package/functions/computation-system-v3/framework/tests/unit/output/SectorCorrelations.json +6 -0
- package/functions/computation-system-v3/framework/tests/unit/output/SignedInUserMirrorHistory.json +6 -0
- package/functions/computation-system-v3/framework/tests/unit/output/SignedInUserPIProfileMetrics.json +4 -0
- package/functions/computation-system-v3/framework/tests/unit/output/SignedInUserProfileMetrics.json +67 -0
- package/functions/computation-system-v3/handlers/dispatcher.js +54 -14
- package/package.json +3 -2
|
@@ -11,11 +11,8 @@ async function handleComputationResultWrite(change, context, config, dependencie
|
|
|
11
11
|
const { logger } = ctx;
|
|
12
12
|
|
|
13
13
|
try {
|
|
14
|
-
// 1. Extract parameters from the context
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
logger?.log('INFO', `[AlertV3/Ingest] Triggered! Params: ${JSON.stringify(context.params)} Path: ${change?.after?.ref?.path}`);
|
|
18
|
-
|
|
14
|
+
// 1. Extract parameters from the context
|
|
15
|
+
let { date, computationName, documentId } = context.params || {};
|
|
19
16
|
|
|
20
17
|
// 2. Load Alert Types if not cached (Cold Start Handling)
|
|
21
18
|
if (!cachedAlertTypes) {
|
|
@@ -43,8 +43,8 @@ module.exports = {
|
|
|
43
43
|
// =========================================================================
|
|
44
44
|
tables: {
|
|
45
45
|
// System Tables
|
|
46
|
-
state: { table: 'computation_state', dataset: '
|
|
47
|
-
results: { table: 'computation_results_v3', dataset: '
|
|
46
|
+
state: { table: 'computation_state', dataset: 'bulltrackers_data' },
|
|
47
|
+
results: { table: 'computation_results_v3', dataset: 'bulltrackers_data' },
|
|
48
48
|
|
|
49
49
|
// Data Sources (Matches V2)
|
|
50
50
|
'portfolio_snapshots': {
|
|
@@ -126,6 +126,8 @@ module.exports = {
|
|
|
126
126
|
time: '02:00',
|
|
127
127
|
timezone: 'UTC'
|
|
128
128
|
},
|
|
129
|
+
// [FIX] -1 prevents "Today" from being scheduled
|
|
130
|
+
maxSchedulingDateOffset: -1,
|
|
129
131
|
dependencyGapMinutes: 5
|
|
130
132
|
},
|
|
131
133
|
|
|
@@ -31,7 +31,6 @@ class StateRepository {
|
|
|
31
31
|
const fullResultsTable = `\`${this.config.bigquery.projectId}.${this.config.bigquery.dataset}.${this.resultsTable}\``;
|
|
32
32
|
const query = `SELECT entity_id, result_data FROM ${fullResultsTable} WHERE date = CAST(@date AS DATE) AND computation_name = @comp AND entity_id IN UNNEST(@ids)`;
|
|
33
33
|
|
|
34
|
-
// [FIX] Removed try/catch. If BQ fails, we must fail.
|
|
35
34
|
const [rows] = await this.bigquery.query({ query, params: { date: dateStr, comp: computationName, ids: entityIds }, location: this.config.bigquery.location });
|
|
36
35
|
const resultMap = {};
|
|
37
36
|
for (const row of rows) {
|
|
@@ -46,7 +45,6 @@ class StateRepository {
|
|
|
46
45
|
const fullResultsTable = `\`${this.config.bigquery.projectId}.${this.config.bigquery.dataset}.${this.resultsTable}\``;
|
|
47
46
|
const query = `SELECT result_data FROM ${fullResultsTable} WHERE date = CAST(@date AS DATE) AND computation_name = @comp AND entity_id = '_global' LIMIT 1`;
|
|
48
47
|
|
|
49
|
-
// [FIX] Removed try/catch
|
|
50
48
|
const [rows] = await this.bigquery.query({ query, params: { date: dateStr, comp: computationName }, location: this.config.bigquery.location });
|
|
51
49
|
if (rows.length === 0) return null;
|
|
52
50
|
let data = rows[0].result_data;
|
|
@@ -55,31 +53,29 @@ class StateRepository {
|
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
async getDailyStatus(dateStr) {
|
|
58
|
-
try
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
// [FIX] REMOVED try/catch.
|
|
57
|
+
// If Firestore is down, we WANT this to throw.
|
|
58
|
+
// Returning an empty Map causes the Planner to believe ALL history is missing,
|
|
59
|
+
// triggering catastrophic backfills.
|
|
60
|
+
|
|
61
|
+
const snapshot = await this.firestore.collectionGroup('runs')
|
|
62
|
+
.where('date', '==', dateStr)
|
|
63
|
+
.get();
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
65
|
+
const map = new Map();
|
|
66
|
+
snapshot.forEach(doc => {
|
|
67
|
+
const data = doc.data();
|
|
68
|
+
const computationName = data.computationName || doc.ref.parent.parent.id;
|
|
69
|
+
|
|
70
|
+
map.set(computationName, {
|
|
71
|
+
status: data.status,
|
|
72
|
+
hash: data.codeHash,
|
|
73
|
+
resultHash: data.resultHash,
|
|
74
|
+
timestamp: data.lastUpdated ? data.lastUpdated.toMillis() : Date.now(),
|
|
75
|
+
metadata: data.metadata || {}
|
|
75
76
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// Control plane failure is acceptable to catch if we want to fallback,
|
|
79
|
-
// but warned loudly.
|
|
80
|
-
this.logger.warn(`[StateRepo] Failed to get daily status (FS): ${e.message}`);
|
|
81
|
-
return new Map();
|
|
82
|
-
}
|
|
77
|
+
});
|
|
78
|
+
return map;
|
|
83
79
|
}
|
|
84
80
|
|
|
85
81
|
async updateStatus({ computation, date, status, hash, resultHash, metadata = {} }) {
|
|
@@ -129,7 +125,7 @@ class StateRepository {
|
|
|
129
125
|
lastUpdated: new Date(),
|
|
130
126
|
startedAt: metadata.startedAt ? new Date(metadata.startedAt) : new Date(),
|
|
131
127
|
totalBatches: metadata.totalBatches || 0,
|
|
132
|
-
finalized: false
|
|
128
|
+
finalized: false
|
|
133
129
|
};
|
|
134
130
|
|
|
135
131
|
await docRef.set(data, { merge: true });
|
|
@@ -161,17 +157,11 @@ class StateRepository {
|
|
|
161
157
|
|
|
162
158
|
return { completedBatches: snapshot.data().count };
|
|
163
159
|
} catch (e) {
|
|
164
|
-
// [FIX] Log the error!
|
|
165
160
|
this.logger.error(`[StateRepo] getCheckpointProgress failed for ${checkpointId}: ${e.message}`);
|
|
166
161
|
return { completedBatches: 0 };
|
|
167
162
|
}
|
|
168
163
|
}
|
|
169
164
|
|
|
170
|
-
/**
|
|
171
|
-
* [FIX] Atomic Transaction to Claim Finalization Rights.
|
|
172
|
-
* Prevents multiple workers from triggering finalization simultaneously.
|
|
173
|
-
* @returns {Promise<boolean>} true if claim successful, false otherwise.
|
|
174
|
-
*/
|
|
175
165
|
async claimFinalization(checkpointId, requiredTotalBatches) {
|
|
176
166
|
const checkpointRef = this.firestore.collection(this.collections.checkpoints).doc(checkpointId);
|
|
177
167
|
|
|
@@ -182,12 +172,10 @@ class StateRepository {
|
|
|
182
172
|
|
|
183
173
|
const data = doc.data();
|
|
184
174
|
|
|
185
|
-
// 1. Check if already finalized or finalizing
|
|
186
175
|
if (data.finalized || data.status === 'finalizing') {
|
|
187
176
|
return false;
|
|
188
177
|
}
|
|
189
178
|
|
|
190
|
-
// 2. Check if actually complete (Double check inside transaction)
|
|
191
179
|
const batchesSnap = await t.get(
|
|
192
180
|
this.firestore.collection(this.collections.checkpoints)
|
|
193
181
|
.doc(checkpointId)
|
|
@@ -198,13 +186,12 @@ class StateRepository {
|
|
|
198
186
|
const currentCount = batchesSnap.data().count;
|
|
199
187
|
|
|
200
188
|
if (currentCount < requiredTotalBatches) {
|
|
201
|
-
return false;
|
|
189
|
+
return false;
|
|
202
190
|
}
|
|
203
191
|
|
|
204
|
-
// 3. Claim it
|
|
205
192
|
t.update(checkpointRef, {
|
|
206
193
|
status: 'finalizing',
|
|
207
|
-
finalized: true,
|
|
194
|
+
finalized: true,
|
|
208
195
|
finalizationStartedAt: new Date()
|
|
209
196
|
});
|
|
210
197
|
|
|
@@ -309,7 +296,6 @@ class StateRepository {
|
|
|
309
296
|
async findZombies(thresholdMinutes) {
|
|
310
297
|
const thresholdDate = new Date(Date.now() - (thresholdMinutes * 60000));
|
|
311
298
|
try {
|
|
312
|
-
// NOTE: Requires Composite Index on (status, lastUpdated)
|
|
313
299
|
const snapshot = await this.firestore.collection(this.collections.checkpoints)
|
|
314
300
|
.where('status', '==', 'running')
|
|
315
301
|
.where('lastUpdated', '<', thresholdDate)
|
|
@@ -328,7 +314,6 @@ class StateRepository {
|
|
|
328
314
|
});
|
|
329
315
|
return zombies;
|
|
330
316
|
} catch (e) {
|
|
331
|
-
// [FIX] Do NOT fail silently. If the DB is down, we must know.
|
|
332
317
|
this.logger.error(`[StateRepo] CRITICAL: findZombies failed: ${e.message}`);
|
|
333
318
|
throw e;
|
|
334
319
|
}
|
|
@@ -339,7 +324,6 @@ class StateRepository {
|
|
|
339
324
|
await docRef.set({
|
|
340
325
|
status: 'recovering',
|
|
341
326
|
lastUpdated: new Date(),
|
|
342
|
-
// Uses atomic increment to track retry count
|
|
343
327
|
attempts: FieldValue.increment(1)
|
|
344
328
|
}, { merge: true });
|
|
345
329
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* FIX: Passes computationName to BQ Adapter for better logging.
|
|
6
6
|
* FIX: Solved Race Condition in Finalization.
|
|
7
7
|
* FIX: Solved Hardcoded Failure Thresholds.
|
|
8
|
+
* FIX: Persists 'blocked' status to prevent planner loops.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
const { RunAnalyzer } = require('./RunAnalyzer');
|
|
@@ -48,6 +49,18 @@ class Coordinator {
|
|
|
48
49
|
|
|
49
50
|
if (analysis.status === 'blocked') {
|
|
50
51
|
this.logger.log(`[Coordinator] Blocked: ${analysis.reason}`);
|
|
52
|
+
|
|
53
|
+
// [FIX] Update state to 'blocked' so Planner knows it's handled.
|
|
54
|
+
if (!dryRun) {
|
|
55
|
+
await this.stateRepo.updateStatus({
|
|
56
|
+
computation: entry.name,
|
|
57
|
+
date,
|
|
58
|
+
status: 'blocked',
|
|
59
|
+
hash: entry.hash,
|
|
60
|
+
metadata: { blockedReason: analysis.reason }
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
return { status: 'blocked', reason: analysis.reason };
|
|
52
65
|
}
|
|
53
66
|
|
|
@@ -122,9 +135,9 @@ class Coordinator {
|
|
|
122
135
|
|
|
123
136
|
if (failures.length > 0 && !dryRun && this.stateRepo.logBatchErrors) { await this.stateRepo.logBatchErrors(checkpointId || 'unknown-checkpoint', failures); }
|
|
124
137
|
|
|
125
|
-
// [FIX] Configurable Failure Thresholds
|
|
126
|
-
const maxFailureRate = this.config.execution?.maxFailureRate ?? 0.15;
|
|
127
|
-
const minFailuresForAbort = this.config.execution?.minFailuresForAbort ?? 5;
|
|
138
|
+
// [FIX] Configurable Failure Thresholds
|
|
139
|
+
const maxFailureRate = this.config.execution?.maxFailureRate ?? 0.15;
|
|
140
|
+
const minFailuresForAbort = this.config.execution?.minFailuresForAbort ?? 5;
|
|
128
141
|
|
|
129
142
|
const failureRate = failures.length / entityIds.length;
|
|
130
143
|
if (entityIds.length > 0 && failures.length >= minFailuresForAbort && failureRate > maxFailureRate) {
|
|
@@ -162,14 +175,12 @@ class Coordinator {
|
|
|
162
175
|
|
|
163
176
|
async tryFinalizeComputation({ computationName, date, checkpointId, totalBatches }) {
|
|
164
177
|
// [FIX] Use Atomic Claim Transaction instead of Check-Then-Act
|
|
165
|
-
// This prevents multiple workers from winning the race to finalize
|
|
166
178
|
const claimed = await this.stateRepo.claimFinalization(checkpointId, totalBatches);
|
|
167
179
|
|
|
168
180
|
if (claimed) {
|
|
169
181
|
return await this.finalizeComputation({ computationName, date, checkpointId });
|
|
170
182
|
}
|
|
171
183
|
|
|
172
|
-
// If not claimed, check if it's because it's still pending or already done
|
|
173
184
|
const progress = await this.stateRepo.getCheckpointProgress(checkpointId);
|
|
174
185
|
return { status: 'pending_or_finalized', progress: `${progress.completedBatches}/${totalBatches}` };
|
|
175
186
|
}
|
|
@@ -178,14 +189,11 @@ class Coordinator {
|
|
|
178
189
|
const entry = this.manifestMap.get(computationName.toLowerCase());
|
|
179
190
|
this.logger.log(`[Coordinator] Finalizing ${entry.name} for ${date}...`);
|
|
180
191
|
|
|
181
|
-
const start = Date.now(); // Track finalization timing
|
|
182
192
|
const dailyStatus = await this.stateRepo.getDailyStatus(date);
|
|
183
193
|
const previousRun = dailyStatus.get(entry.name);
|
|
184
194
|
|
|
185
|
-
// Finalize storage (merge results)
|
|
186
195
|
const { resultHash } = await this.storage.finalizeResults(date, entry) || {};
|
|
187
196
|
|
|
188
|
-
// Update State
|
|
189
197
|
await this.stateRepo.updateStatus({
|
|
190
198
|
computation: entry.name,
|
|
191
199
|
date,
|
|
@@ -195,14 +203,11 @@ class Coordinator {
|
|
|
195
203
|
metadata: { checkpointId, finalizedAt: new Date() }
|
|
196
204
|
});
|
|
197
205
|
|
|
198
|
-
// [ADD] Discord Alert
|
|
199
206
|
if (this.notifier) {
|
|
200
|
-
|
|
201
|
-
const duration = 0; // Simple placeholder, or calculate from checkpoint start time
|
|
207
|
+
const duration = 0;
|
|
202
208
|
this.notifier.reportSuccess(entry.name, date, duration, resultHash);
|
|
203
209
|
}
|
|
204
210
|
|
|
205
|
-
// Trigger Cascades
|
|
206
211
|
if (resultHash && (!previousRun || resultHash !== previousRun.resultHash)) {
|
|
207
212
|
await this._triggerCascading(entry, date);
|
|
208
213
|
}
|
|
@@ -232,7 +237,6 @@ class Coordinator {
|
|
|
232
237
|
const checkpointId = `${entry.name}-${date}-${Date.now()}`;
|
|
233
238
|
let batchCounter = 0;
|
|
234
239
|
|
|
235
|
-
// [FIX] Added hash to task name to prevent collision on re-runs
|
|
236
240
|
const configHash = entry.hash ? entry.hash.substring(0, 8) : 'nohash';
|
|
237
241
|
|
|
238
242
|
const createBatchTask = (batchIds) => {
|
|
@@ -365,7 +369,6 @@ class Coordinator {
|
|
|
365
369
|
const totalBatches = batchCounter;
|
|
366
370
|
|
|
367
371
|
if (remainingBatches.length === 0) {
|
|
368
|
-
// [ADD] Explicit Diagnostic Log
|
|
369
372
|
this.logger.log(`[Coordinator] [Recovery] Diagnosis for ${entry.name}: All ${totalBatches} batches complete. Missing Finalizer. Triggering immediately.`);
|
|
370
373
|
|
|
371
374
|
await this._triggerTryFinalizeTask(entry, date, checkpointId, 0, totalBatches, true);
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* - Allows any pass to be scheduled if Hash Mismatch is detected.
|
|
5
5
|
* - Enforces execution order via ETAs: Pass N runs 10 mins after Pass N-1.
|
|
6
6
|
* - FIX: Enforces UTC date arithmetic.
|
|
7
|
+
* - FIX: T-1 Scheduling Logic.
|
|
8
|
+
* - FIX: Handles 'blocked' status properly.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
const { ScheduleValidator } = require('./ScheduleValidator');
|
|
@@ -22,10 +24,10 @@ class TaskScheduler {
|
|
|
22
24
|
this.LOOKBACK_DAYS = config.planningLookbackDays ?? 7;
|
|
23
25
|
this.LOOKAHEAD_HOURS = config.planningLookaheadHours ?? 24;
|
|
24
26
|
this.ZOMBIE_THRESHOLD_MINUTES = config.zombieThresholdMinutes ?? 15;
|
|
25
|
-
|
|
26
|
-
// 10 Minute buffer per pass level to ensure upstream dependencies
|
|
27
|
-
// have time to finish (or trigger cascades) before downstream runs.
|
|
28
27
|
this.PASS_DELAY_SECONDS = config.passDelaySeconds ?? 600;
|
|
28
|
+
|
|
29
|
+
// [FIX] Default to -1 (Yesterday) to enforce T-1 rule
|
|
30
|
+
this.MAX_DATE_OFFSET = config.scheduling?.maxSchedulingDateOffset ?? -1;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
async planComputations(referenceDate = new Date()) {
|
|
@@ -41,6 +43,7 @@ class TaskScheduler {
|
|
|
41
43
|
|
|
42
44
|
this.logger.log(`[Planner] Reconciling: ${windowStart.toISOString()} to ${windowEnd.toISOString()}`);
|
|
43
45
|
|
|
46
|
+
// [FIX] Use Date Generator that respects MAX_DATE_OFFSET
|
|
44
47
|
const targetDates = this._generateDateRange(windowStart, windowEnd);
|
|
45
48
|
const tasksToSchedule = new Map();
|
|
46
49
|
const stats = { checked: 0, missing: 0, mismatched: 0, scheduled: 0, exists: 0, deleted: 0 };
|
|
@@ -50,20 +53,22 @@ class TaskScheduler {
|
|
|
50
53
|
const dailyStatus = await this.stateRepo.getDailyStatus(dateStr);
|
|
51
54
|
|
|
52
55
|
for (const entry of this.manifest) {
|
|
53
|
-
// [UPDATE] Removed "entry.pass !== 1" check.
|
|
54
|
-
// We now evaluate ALL passes for potential fixes/backfills.
|
|
55
|
-
|
|
56
56
|
const effectiveSchedule = this.validator.parseSchedule(entry.schedule);
|
|
57
57
|
if (!this.validator.shouldRunOnDate(effectiveSchedule, dateObj)) continue;
|
|
58
58
|
|
|
59
59
|
stats.checked++;
|
|
60
60
|
const statusEntry = dailyStatus instanceof Map ? dailyStatus.get(entry.name) : dailyStatus[entry.name];
|
|
61
|
+
|
|
61
62
|
const lastRunHash = statusEntry?.hash;
|
|
62
63
|
const rawStatus = statusEntry?.status;
|
|
63
64
|
const status = rawStatus ? rawStatus.toLowerCase() : null;
|
|
64
65
|
|
|
65
66
|
let reason = null;
|
|
66
67
|
|
|
68
|
+
// [FIX] Logic to handle 'blocked' status.
|
|
69
|
+
// If it is 'blocked', we generally treat it as handled (do nothing).
|
|
70
|
+
// It will only be retried if it is truly missing or failed.
|
|
71
|
+
|
|
67
72
|
if (!statusEntry || status === 'pending') {
|
|
68
73
|
reason = 'MISSING_RUN';
|
|
69
74
|
stats.missing++;
|
|
@@ -72,7 +77,8 @@ class TaskScheduler {
|
|
|
72
77
|
reason = 'RETRY_FAILED';
|
|
73
78
|
stats.missing++;
|
|
74
79
|
}
|
|
75
|
-
|
|
80
|
+
// Check mismatch only if not running and not blocked (blocked means we are waiting for something else)
|
|
81
|
+
else if (lastRunHash !== entry.hash && status !== 'running' && status !== 'blocked') {
|
|
76
82
|
reason = 'HASH_MISMATCH';
|
|
77
83
|
stats.mismatched++;
|
|
78
84
|
}
|
|
@@ -81,17 +87,13 @@ class TaskScheduler {
|
|
|
81
87
|
const taskKey = `root-${entry.name}-${dateStr}-${entry.hash}`;
|
|
82
88
|
|
|
83
89
|
if (!tasksToSchedule.has(taskKey)) {
|
|
84
|
-
// 1. Calculate Base Window
|
|
90
|
+
// 1. Calculate Base Window
|
|
85
91
|
const baseRunAt = this._getNextRunWindow(effectiveSchedule, dateObj);
|
|
86
92
|
|
|
87
93
|
// 2. Calculate Topological Delay
|
|
88
|
-
// If Pass 1 runs at T, Pass 2 runs at T + 10m, Pass 3 at T + 20m.
|
|
89
|
-
// This allows Pass 1 to finish and trigger a natural Cascade for Pass 2
|
|
90
|
-
// BEFORE the scheduled Pass 2 task fires (avoiding redundancy/conflicts).
|
|
91
94
|
const passDelay = (entry.pass - 1) * this.PASS_DELAY_SECONDS;
|
|
92
95
|
|
|
93
96
|
// 3. Determine Final Execution Time
|
|
94
|
-
// If baseRunAt is 0 (meaning "ASAP" / window passed), we base delay on NOW.
|
|
95
97
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
96
98
|
const effectiveBase = baseRunAt === 0 ? nowSeconds : baseRunAt;
|
|
97
99
|
const runAtSeconds = effectiveBase + passDelay;
|
|
@@ -138,7 +140,6 @@ class TaskScheduler {
|
|
|
138
140
|
|
|
139
141
|
/**
|
|
140
142
|
* Watchdog: Find and recover zombie tasks.
|
|
141
|
-
* UPDATED: Now diagnoses the cause of the zombie state.
|
|
142
143
|
*/
|
|
143
144
|
async runWatchdog() {
|
|
144
145
|
if (!this.stateRepo.findZombies) {
|
|
@@ -153,17 +154,16 @@ class TaskScheduler {
|
|
|
153
154
|
|
|
154
155
|
this.logger.log(`[Watchdog] Found ${recoverable.length} zombies.`);
|
|
155
156
|
|
|
156
|
-
// [ADD] Diagnostic Phase
|
|
157
|
+
// [ADD] Diagnostic Phase
|
|
157
158
|
const diagnostics = await Promise.all(recoverable.map(async z => {
|
|
158
159
|
try {
|
|
159
|
-
// Use existing repo methods to peek at state
|
|
160
160
|
const progress = await this.stateRepo.getCheckpointProgress(z.checkpointId);
|
|
161
161
|
const checkpoint = await this.stateRepo.loadCheckpoint(z.checkpointId);
|
|
162
162
|
|
|
163
163
|
const total = checkpoint?.totalBatches || 0;
|
|
164
164
|
const completed = progress.completedBatches || 0;
|
|
165
165
|
let reason = 'Unknown Stuck State';
|
|
166
|
-
let type = 'warn';
|
|
166
|
+
let type = 'warn';
|
|
167
167
|
|
|
168
168
|
if (total > 0 && completed >= total) {
|
|
169
169
|
reason = '✅ All Batches Done (Finalizer Failed)';
|
|
@@ -183,16 +183,25 @@ class TaskScheduler {
|
|
|
183
183
|
}
|
|
184
184
|
}));
|
|
185
185
|
|
|
186
|
-
// Notify Discord with enhanced info
|
|
187
186
|
if (this.notifier) {
|
|
188
187
|
await this.notifier.reportZombies(recoverable.length, diagnostics);
|
|
189
188
|
}
|
|
190
189
|
|
|
191
|
-
// Claim zombies
|
|
192
190
|
await Promise.all(recoverable.map(z => this.stateRepo.claimZombie(z.checkpointId)));
|
|
193
191
|
|
|
194
|
-
// Re-dispatch logic
|
|
195
192
|
const recoveryTasks = recoverable.map(z => {
|
|
193
|
+
// [FIX] Enforce T-1 Rule on Zombies too to avoid resurrecting future tasks
|
|
194
|
+
const zDate = new Date(z.date);
|
|
195
|
+
const now = new Date();
|
|
196
|
+
const maxAllowed = new Date(now);
|
|
197
|
+
maxAllowed.setUTCDate(now.getUTCDate() + this.MAX_DATE_OFFSET);
|
|
198
|
+
maxAllowed.setUTCHours(23, 59, 59, 999);
|
|
199
|
+
|
|
200
|
+
if (zDate > maxAllowed) {
|
|
201
|
+
this.logger.warn(`[Watchdog] Skipping recovery for ${z.name}@${z.date} (Outside Scheduling Window)`);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
196
205
|
const entry = this.manifestMap.get(z.name);
|
|
197
206
|
if (!entry) return null;
|
|
198
207
|
|
|
@@ -216,8 +225,17 @@ class TaskScheduler {
|
|
|
216
225
|
_generateDateRange(start, end) {
|
|
217
226
|
const dates = [];
|
|
218
227
|
let cur = new Date(start);
|
|
228
|
+
|
|
229
|
+
// [FIX] Cap the scheduling window based on config (Default T-1)
|
|
230
|
+
const now = new Date();
|
|
231
|
+
const maxAllowed = new Date(now);
|
|
232
|
+
maxAllowed.setUTCDate(now.getUTCDate() + this.MAX_DATE_OFFSET);
|
|
233
|
+
maxAllowed.setUTCHours(23, 59, 59, 999);
|
|
234
|
+
|
|
219
235
|
while (cur <= end) {
|
|
220
|
-
|
|
236
|
+
if (cur <= maxAllowed) {
|
|
237
|
+
dates.push(new Date(cur));
|
|
238
|
+
}
|
|
221
239
|
cur.setUTCDate(cur.getUTCDate() + 1);
|
|
222
240
|
}
|
|
223
241
|
return dates;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"vectors": {},
|
|
3
|
+
"piCount": 0,
|
|
4
|
+
"sectorDimensions": [
|
|
5
|
+
"Technology",
|
|
6
|
+
"Healthcare",
|
|
7
|
+
"Financial Services",
|
|
8
|
+
"Consumer Cyclical",
|
|
9
|
+
"Industrials",
|
|
10
|
+
"Energy",
|
|
11
|
+
"Utilities",
|
|
12
|
+
"Real Estate",
|
|
13
|
+
"Communication Services",
|
|
14
|
+
"Consumer Defensive",
|
|
15
|
+
"Basic Materials",
|
|
16
|
+
"Crypto",
|
|
17
|
+
"Commodities",
|
|
18
|
+
"ETF",
|
|
19
|
+
"Other"
|
|
20
|
+
],
|
|
21
|
+
"computedAt": "2023-01-01"
|
|
22
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"username": null,
|
|
3
|
+
"socialEngagement": {
|
|
4
|
+
"chartType": "line",
|
|
5
|
+
"data": []
|
|
6
|
+
},
|
|
7
|
+
"profitablePositions": {
|
|
8
|
+
"chartType": "bar",
|
|
9
|
+
"data": []
|
|
10
|
+
},
|
|
11
|
+
"topWinningPositions": {
|
|
12
|
+
"chartType": "table",
|
|
13
|
+
"data": []
|
|
14
|
+
},
|
|
15
|
+
"sectorPerformance": {
|
|
16
|
+
"bestSector": null,
|
|
17
|
+
"worstSector": null,
|
|
18
|
+
"bestSectorProfit": 0,
|
|
19
|
+
"worstSectorProfit": 0
|
|
20
|
+
},
|
|
21
|
+
"sectorExposure": {
|
|
22
|
+
"chartType": "pie",
|
|
23
|
+
"data": {}
|
|
24
|
+
},
|
|
25
|
+
"assetExposure": {
|
|
26
|
+
"chartType": "pie",
|
|
27
|
+
"data": {}
|
|
28
|
+
},
|
|
29
|
+
"portfolioSummary": {
|
|
30
|
+
"totalInvested": 0,
|
|
31
|
+
"totalProfit": 0,
|
|
32
|
+
"profitPercent": 0
|
|
33
|
+
},
|
|
34
|
+
"rankingsData": {
|
|
35
|
+
"aum": 0,
|
|
36
|
+
"riskScore": 0,
|
|
37
|
+
"gain": 0,
|
|
38
|
+
"copiers": 0,
|
|
39
|
+
"winRatio": 0,
|
|
40
|
+
"trades": 0
|
|
41
|
+
},
|
|
42
|
+
"ratingsData": {
|
|
43
|
+
"averageRating": 0,
|
|
44
|
+
"totalRatings": 0,
|
|
45
|
+
"ratingsOverTime": {
|
|
46
|
+
"chartType": "line",
|
|
47
|
+
"data": []
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"pageViewsData": {
|
|
51
|
+
"totalViews": 0,
|
|
52
|
+
"uniqueViewers": 0,
|
|
53
|
+
"viewsOverTime": {
|
|
54
|
+
"chartType": "bar",
|
|
55
|
+
"data": []
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"watchlistData": {
|
|
59
|
+
"totalUsers": 0,
|
|
60
|
+
"watchlistOverTime": {
|
|
61
|
+
"chartType": "line",
|
|
62
|
+
"data": []
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"alertHistoryData": {
|
|
66
|
+
"triggeredAlerts": [],
|
|
67
|
+
"alertCountsOverTime": {
|
|
68
|
+
"chartType": "bar",
|
|
69
|
+
"data": []
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"sectorExposureOverTime": {
|
|
73
|
+
"chartType": "line",
|
|
74
|
+
"data": []
|
|
75
|
+
},
|
|
76
|
+
"assetExposureOverTime": {
|
|
77
|
+
"chartType": "line",
|
|
78
|
+
"data": []
|
|
79
|
+
},
|
|
80
|
+
"alertMetrics": {
|
|
81
|
+
"totalLast7Days": 0,
|
|
82
|
+
"breakdown": {}
|
|
83
|
+
}
|
|
84
|
+
}
|
package/functions/computation-system-v3/framework/tests/unit/output/SignedInUserProfileMetrics.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"socialEngagement": {
|
|
3
|
+
"chartType": "line",
|
|
4
|
+
"data": []
|
|
5
|
+
},
|
|
6
|
+
"myPosts": {
|
|
7
|
+
"chartType": "feed",
|
|
8
|
+
"data": []
|
|
9
|
+
},
|
|
10
|
+
"profitablePositions": {
|
|
11
|
+
"chartType": "bar",
|
|
12
|
+
"data": []
|
|
13
|
+
},
|
|
14
|
+
"topWinningPositions": {
|
|
15
|
+
"chartType": "table",
|
|
16
|
+
"data": []
|
|
17
|
+
},
|
|
18
|
+
"sectorPerformance": {
|
|
19
|
+
"bestSector": null,
|
|
20
|
+
"worstSector": null,
|
|
21
|
+
"bestSectorProfit": 0,
|
|
22
|
+
"worstSectorProfit": 0
|
|
23
|
+
},
|
|
24
|
+
"sectorExposure": {
|
|
25
|
+
"chartType": "pie",
|
|
26
|
+
"data": {}
|
|
27
|
+
},
|
|
28
|
+
"assetExposure": {
|
|
29
|
+
"chartType": "pie",
|
|
30
|
+
"data": {}
|
|
31
|
+
},
|
|
32
|
+
"portfolioSummary": {
|
|
33
|
+
"totalInvested": 0,
|
|
34
|
+
"totalProfit": 0,
|
|
35
|
+
"profitPercent": 0
|
|
36
|
+
},
|
|
37
|
+
"copiedPIs": {
|
|
38
|
+
"chartType": "cards",
|
|
39
|
+
"data": []
|
|
40
|
+
},
|
|
41
|
+
"recommendedPIs": {
|
|
42
|
+
"chartType": "cards",
|
|
43
|
+
"data": []
|
|
44
|
+
},
|
|
45
|
+
"recentlyViewedPages": {
|
|
46
|
+
"chartType": "list",
|
|
47
|
+
"data": []
|
|
48
|
+
},
|
|
49
|
+
"alertGraphs": {
|
|
50
|
+
"chartType": "line",
|
|
51
|
+
"data": []
|
|
52
|
+
},
|
|
53
|
+
"performanceGraphics": {
|
|
54
|
+
"chartType": "composite",
|
|
55
|
+
"data": {
|
|
56
|
+
"winRate": 0,
|
|
57
|
+
"avgWin": 0,
|
|
58
|
+
"avgLoss": 0,
|
|
59
|
+
"profitFactor": 0,
|
|
60
|
+
"dailyPnL": []
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"exposureGraphics": {
|
|
64
|
+
"chartType": "composite",
|
|
65
|
+
"data": {}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -1,33 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* V3 Dispatcher Handler
|
|
3
|
-
*
|
|
2
|
+
* V3 Dispatcher Handler (FIXED & LOGGING ENHANCED)
|
|
3
|
+
* Routes tasks and ensures errors are returned to Cloud Tasks logs.
|
|
4
4
|
*/
|
|
5
5
|
exports.dispatcher = async (req, res) => {
|
|
6
|
+
// [DEBUG] Log the incoming trigger to match Cloud Tasks timestamps
|
|
7
|
+
const taskId = req.get('X-CloudTasks-TaskName') || 'unknown-task';
|
|
8
|
+
console.log(`[Dispatcher] Received task ${taskId} type=${req.body.type || 'unknown'}`);
|
|
9
|
+
|
|
6
10
|
try {
|
|
7
|
-
// LAZY LOAD to prevent circular dependency
|
|
8
11
|
const system = require('../index');
|
|
9
|
-
|
|
10
12
|
await system.initialize();
|
|
11
13
|
|
|
12
|
-
const { computation, date,
|
|
14
|
+
const { type, computation, date, entityIds, ...params } = req.body;
|
|
13
15
|
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
// 1. Route Worker Batches
|
|
17
|
+
if (type === 'worker-batch') {
|
|
18
|
+
if (!entityIds) throw new Error('Missing entityIds');
|
|
19
|
+
const result = await system.coordinatorInstance.processBatch({
|
|
20
|
+
computationName: computation, date, entityIds, ...params
|
|
21
|
+
});
|
|
22
|
+
return res.status(200).json(result);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 2. Route Finalizer
|
|
26
|
+
if (type === 'try-finalize') {
|
|
27
|
+
const result = await system.coordinatorInstance.tryFinalizeComputation({
|
|
28
|
+
computationName: computation, date, ...params
|
|
29
|
+
});
|
|
30
|
+
return res.status(200).json(result);
|
|
31
|
+
}
|
|
17
32
|
|
|
18
|
-
|
|
19
|
-
|
|
33
|
+
// 3. Route Cascade
|
|
34
|
+
if (type === 'cascade-trigger') {
|
|
35
|
+
const result = await system.runComputation({
|
|
36
|
+
computationName: computation, date, force: params.force || false
|
|
37
|
+
});
|
|
38
|
+
return res.status(200).json(result);
|
|
20
39
|
}
|
|
21
40
|
|
|
41
|
+
// 4. Default: Coordinator (Root Triggers)
|
|
42
|
+
const compName = computation || req.body.computationName;
|
|
43
|
+
if (!compName) throw new Error('Missing computation name');
|
|
44
|
+
|
|
22
45
|
const result = await system.runComputation({
|
|
23
46
|
computationName: compName,
|
|
24
|
-
date:
|
|
25
|
-
dryRun: dryRun || false
|
|
47
|
+
date: date || new Date().toISOString().split('T')[0],
|
|
48
|
+
dryRun: params.dryRun || false,
|
|
49
|
+
...params
|
|
26
50
|
});
|
|
27
51
|
|
|
52
|
+
// [LOGIC FIX] If the result is 'blocked', return 200 (OK) so Cloud Tasks STOPS retrying.
|
|
53
|
+
// If we return 500, Cloud Tasks will retry forever (or until max attempts).
|
|
54
|
+
if (result.status === 'blocked') {
|
|
55
|
+
console.log(`[Dispatcher] Task Blocked (Handling as Success to stop retry): ${result.reason}`);
|
|
56
|
+
return res.status(200).json(result);
|
|
57
|
+
}
|
|
58
|
+
|
|
28
59
|
return res.status(200).json(result);
|
|
60
|
+
|
|
29
61
|
} catch (e) {
|
|
30
|
-
|
|
31
|
-
|
|
62
|
+
// [CRITICAL] Log error to Cloud Logging
|
|
63
|
+
console.error(`[V3-Dispatcher] CRITICAL ERROR on ${taskId}:`, e);
|
|
64
|
+
|
|
65
|
+
// Return 500 with the error message.
|
|
66
|
+
// Cloud Tasks 'attemptResponseLog' usually captures the first few bytes of the body.
|
|
67
|
+
return res.status(500).json({
|
|
68
|
+
error: e.message,
|
|
69
|
+
stack: e.stack ? e.stack.split('\n')[0] : 'no-stack',
|
|
70
|
+
taskId
|
|
71
|
+
});
|
|
32
72
|
}
|
|
33
73
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bulltrackers-module",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.809",
|
|
4
4
|
"description": "Helper Functions for Bulltrackers.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"p-limit": "^3.1.0",
|
|
58
58
|
"require-all": "^3.0.0",
|
|
59
59
|
"sharedsetup": "latest",
|
|
60
|
-
"
|
|
60
|
+
"supertest": "^7.2.2",
|
|
61
|
+
"zod": "^4.3.6"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
63
64
|
"bulltracker-deployer": "file:../bulltracker-deployer",
|