bulltrackers-module 1.0.473 → 1.0.475
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/WorkflowOrchestrator.js +6 -1
- package/functions/computation-system/executors/StandardExecutor.js +8 -0
- package/functions/computation-system/helpers/computation_worker.js +28 -19
- package/functions/computation-system/helpers/on_demand_helpers.js +151 -0
- package/functions/computation-system/workflows/data_feeder_pipeline.yaml +147 -114
- package/functions/etoro-price-fetcher/helpers/handler_helpers.js +24 -0
- package/functions/fetch-insights/helpers/handler_helpers.js +24 -0
- package/functions/generic-api/user-api/helpers/on_demand_fetch_helpers.js +1 -0
- package/functions/generic-api/user-api/helpers/user_sync_helpers.js +1 -0
- package/functions/generic-api/user-api/helpers/verification_helpers.js +41 -17
- package/functions/social-orchestrator/helpers/orchestrator_helpers.js +47 -6
- package/functions/social-task-handler/helpers/handler_helpers.js +4 -2
- package/functions/task-engine/handler_creator.js +73 -3
- package/functions/task-engine/helpers/popular_investor_helpers.js +227 -109
- package/functions/task-engine/helpers/social_helpers.js +282 -0
- package/functions/task-engine/utils/task_engine_utils.js +91 -11
- package/package.json +1 -1
|
@@ -168,12 +168,17 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
168
168
|
return report;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
async function executeDispatchTask(dateStr, pass, targetComputation, config, dependencies, computationManifest, previousCategory = null, dependencyResultHashes = {}) {
|
|
171
|
+
async function executeDispatchTask(dateStr, pass, targetComputation, config, dependencies, computationManifest, previousCategory = null, dependencyResultHashes = {}, metadata = {}) {
|
|
172
172
|
const { logger } = dependencies;
|
|
173
173
|
const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
|
|
174
174
|
const calcManifest = manifestMap.get(normalizeName(targetComputation));
|
|
175
175
|
|
|
176
176
|
if (!calcManifest) throw new Error(`Calc '${targetComputation}' not found.`);
|
|
177
|
+
|
|
178
|
+
// Merge runtime metadata (like targetCid) into the manifest
|
|
179
|
+
// This allows the Executor to access it via 'calcInstance.manifest'
|
|
180
|
+
Object.assign(calcManifest, metadata);
|
|
181
|
+
|
|
177
182
|
calcManifest.dependencyResultHashes = dependencyResultHashes;
|
|
178
183
|
|
|
179
184
|
const rootData = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
|
|
@@ -235,6 +235,14 @@ class StandardExecutor {
|
|
|
235
235
|
let chunkFailures = 0;
|
|
236
236
|
|
|
237
237
|
for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
|
|
238
|
+
// --- OPTIMIZATION: TARGET SPECIFIC USER ---
|
|
239
|
+
// If the request contains a targetCid, skip all other users immediately
|
|
240
|
+
if (metadata.targetCid && String(userId) !== String(metadata.targetCid)) {
|
|
241
|
+
if (stats) stats.skippedUsers++;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
// ------------------------------------------
|
|
245
|
+
|
|
238
246
|
const yesterdayPortfolio = yesterdayPortfolioData ? yesterdayPortfolioData[userId] : null;
|
|
239
247
|
const todayHistory = historyData ? historyData[userId] : null;
|
|
240
248
|
|
|
@@ -93,7 +93,7 @@ function startMemoryHeartbeat(db, ledgerPath, workerId, computationName, traceId
|
|
|
93
93
|
/**
|
|
94
94
|
* STRICT IDEMPOTENCY GATE
|
|
95
95
|
*/
|
|
96
|
-
async function checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerId) {
|
|
96
|
+
async function checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerId, onDemand = false) {
|
|
97
97
|
const docRef = db.doc(ledgerPath);
|
|
98
98
|
|
|
99
99
|
try {
|
|
@@ -103,25 +103,27 @@ async function checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerI
|
|
|
103
103
|
if (doc.exists) {
|
|
104
104
|
const data = doc.data();
|
|
105
105
|
|
|
106
|
-
// [
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
106
|
+
// [ON-DEMAND OVERRIDE] If this is an on-demand request, force re-run even if completed
|
|
107
|
+
if (onDemand && ['COMPLETED', 'FAILED', 'CRASH'].includes(data.status)) {
|
|
108
|
+
// Force overwrite - on-demand requests should always run
|
|
109
|
+
// Log will be handled by caller
|
|
110
|
+
// Continue to claim lease below
|
|
111
|
+
} else {
|
|
112
|
+
// [FIX] Only block if it's the EXACT SAME Dispatch ID (Duplicate Delivery)
|
|
113
|
+
if (['COMPLETED', 'FAILED', 'CRASH'].includes(data.status)) {
|
|
114
|
+
if (data.dispatchId === dispatchId) {
|
|
115
|
+
return { shouldRun: false, reason: `Task already in terminal state: ${data.status}` };
|
|
116
|
+
}
|
|
117
|
+
// If dispatchId differs, we allow the overwrite (Re-Run).
|
|
118
|
+
// The Dispatcher is the authority; if it sent a message, we run it.
|
|
116
119
|
}
|
|
117
|
-
// If dispatchId differs, we allow the overwrite (Re-Run).
|
|
118
|
-
// The Dispatcher is the authority; if it sent a message, we run it.
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
if (data.status === 'IN_PROGRESS' && data.dispatchId === dispatchId) {
|
|
122
123
|
return { shouldRun: false, reason: 'Duplicate delivery: Task already IN_PROGRESS with same ID.' };
|
|
123
124
|
}
|
|
124
|
-
if (data.status === 'IN_PROGRESS') {
|
|
125
|
+
if (data.status === 'IN_PROGRESS' && !onDemand) {
|
|
126
|
+
// On-demand can break locks if needed, but regular requests should wait
|
|
125
127
|
return { shouldRun: false, reason: 'Collision: Task currently IN_PROGRESS by another worker.' };
|
|
126
128
|
}
|
|
127
129
|
}
|
|
@@ -130,7 +132,8 @@ async function checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerI
|
|
|
130
132
|
status: 'IN_PROGRESS',
|
|
131
133
|
workerId: workerId,
|
|
132
134
|
dispatchId: dispatchId || 'unknown',
|
|
133
|
-
startedAt: new Date()
|
|
135
|
+
startedAt: new Date(),
|
|
136
|
+
onDemand: onDemand || false
|
|
134
137
|
};
|
|
135
138
|
|
|
136
139
|
t.set(docRef, lease, { merge: true });
|
|
@@ -151,8 +154,9 @@ async function handleComputationTask(message, config, dependencies) {
|
|
|
151
154
|
|
|
152
155
|
if (!data || data.action !== 'RUN_COMPUTATION_DATE') return;
|
|
153
156
|
|
|
154
|
-
const { date, pass, computation, previousCategory, triggerReason, dispatchId, dependencyResultHashes, resources, traceContext } = data;
|
|
157
|
+
const { date, pass, computation, previousCategory, triggerReason, dispatchId, dependencyResultHashes, resources, traceContext, metadata } = data;
|
|
155
158
|
const resourceTier = resources || 'standard';
|
|
159
|
+
const onDemand = metadata?.onDemand === true || false; // Extract on-demand flag
|
|
156
160
|
const ledgerPath = `computation_audit_ledger/${date}/passes/${pass}/tasks/${computation}`;
|
|
157
161
|
const workerId = process.env.K_REVISION || os.hostname();
|
|
158
162
|
|
|
@@ -174,12 +178,16 @@ async function handleComputationTask(message, config, dependencies) {
|
|
|
174
178
|
const runDeps = { ...dependencies, logger };
|
|
175
179
|
const db = dependencies.db;
|
|
176
180
|
|
|
177
|
-
// --- STEP 1: IDEMPOTENCY CHECK ---
|
|
178
|
-
const gate = await checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerId);
|
|
181
|
+
// --- STEP 1: IDEMPOTENCY CHECK (with on-demand override) ---
|
|
182
|
+
const gate = await checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerId, onDemand);
|
|
179
183
|
if (!gate.shouldRun) {
|
|
180
184
|
logger.log('WARN', `[Worker] 🛑 Idempotency Gate: Skipping ${computation}. Reason: ${gate.reason}`);
|
|
181
185
|
return;
|
|
182
186
|
}
|
|
187
|
+
|
|
188
|
+
if (onDemand) {
|
|
189
|
+
logger.log('INFO', `[Worker] 🔄 On-demand request: Forcing re-run of ${computation} for ${date}`);
|
|
190
|
+
}
|
|
183
191
|
|
|
184
192
|
logger.log('INFO', `[Worker] 📥 Task: ${computation} (${date}) [Tier: ${resourceTier}] [ID: ${dispatchId}]`);
|
|
185
193
|
|
|
@@ -195,7 +203,8 @@ async function handleComputationTask(message, config, dependencies) {
|
|
|
195
203
|
|
|
196
204
|
const result = await executeDispatchTask(
|
|
197
205
|
date, pass, computation, config, runDeps,
|
|
198
|
-
manifest, previousCategory, dependencyResultHashes
|
|
206
|
+
manifest, previousCategory, dependencyResultHashes,
|
|
207
|
+
metadata // Pass metadata (including targetCid) to orchestrator
|
|
199
208
|
);
|
|
200
209
|
|
|
201
210
|
heartbeats.stop();
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Helpers for on-demand computation requests
|
|
3
|
+
* Handles dependency chain resolution and ordered triggering
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { getManifest } = require('../topology/ManifestLoader');
|
|
7
|
+
const { normalizeName } = require('../utils/utils');
|
|
8
|
+
|
|
9
|
+
// Import calculations package (matching computation_worker.js pattern)
|
|
10
|
+
let calculationPackage;
|
|
11
|
+
try {
|
|
12
|
+
calculationPackage = require('aiden-shared-calculations-unified');
|
|
13
|
+
} catch (e) {
|
|
14
|
+
throw new Error(`Failed to load calculations package: ${e.message}`);
|
|
15
|
+
}
|
|
16
|
+
const calculations = calculationPackage.calculations;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolves all dependencies for a given computation and returns them grouped by pass
|
|
20
|
+
* @param {string} computationName - The target computation name
|
|
21
|
+
* @param {Array} manifest - The computation manifest
|
|
22
|
+
* @returns {Array<{pass: number, computations: Array<string>}>} Dependencies grouped by pass
|
|
23
|
+
*/
|
|
24
|
+
function resolveDependencyChain(computationName, manifest) {
|
|
25
|
+
const manifestMap = new Map(manifest.map(c => [normalizeName(c.name), c]));
|
|
26
|
+
const normalizedTarget = normalizeName(computationName);
|
|
27
|
+
|
|
28
|
+
const targetCalc = manifestMap.get(normalizedTarget);
|
|
29
|
+
if (!targetCalc) {
|
|
30
|
+
throw new Error(`Computation ${computationName} not found in manifest`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Build adjacency list (dependencies map)
|
|
34
|
+
const adjacency = new Map();
|
|
35
|
+
for (const calc of manifest) {
|
|
36
|
+
const normName = normalizeName(calc.name);
|
|
37
|
+
adjacency.set(normName, (calc.dependencies || []).map(d => normalizeName(d)));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Get all dependencies recursively
|
|
41
|
+
const required = new Set([normalizedTarget]);
|
|
42
|
+
const queue = [normalizedTarget];
|
|
43
|
+
|
|
44
|
+
while (queue.length > 0) {
|
|
45
|
+
const calcName = queue.shift();
|
|
46
|
+
const dependencies = adjacency.get(calcName) || [];
|
|
47
|
+
for (const dep of dependencies) {
|
|
48
|
+
if (!required.has(dep)) {
|
|
49
|
+
required.add(dep);
|
|
50
|
+
queue.push(dep);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Group by pass
|
|
56
|
+
const byPass = new Map();
|
|
57
|
+
for (const calcName of required) {
|
|
58
|
+
const calc = manifestMap.get(calcName);
|
|
59
|
+
if (calc) {
|
|
60
|
+
const pass = calc.pass || 1;
|
|
61
|
+
if (!byPass.has(pass)) {
|
|
62
|
+
byPass.set(pass, []);
|
|
63
|
+
}
|
|
64
|
+
byPass.get(pass).push(calc.name); // Use original name, not normalized
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Convert to sorted array
|
|
69
|
+
const passes = Array.from(byPass.entries())
|
|
70
|
+
.sort((a, b) => a[0] - b[0])
|
|
71
|
+
.map(([pass, computations]) => ({ pass, computations }));
|
|
72
|
+
|
|
73
|
+
return passes;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Triggers computations for a given date, handling dependencies in order
|
|
78
|
+
* @param {string} targetComputation - The target computation to run
|
|
79
|
+
* @param {string} date - The date string (YYYY-MM-DD)
|
|
80
|
+
* @param {object} dependencies - Contains pubsub, logger, etc.
|
|
81
|
+
* @param {object} config - Computation system config
|
|
82
|
+
* @param {object} metadata - Additional metadata for the computation request
|
|
83
|
+
* @returns {Promise<Array>} Array of triggered computation messages
|
|
84
|
+
*/
|
|
85
|
+
async function triggerComputationWithDependencies(targetComputation, date, dependencies, config, metadata = {}) {
|
|
86
|
+
const { pubsub, logger } = dependencies;
|
|
87
|
+
const computationTopic = config.computationTopicStandard || 'computation-tasks';
|
|
88
|
+
const topic = pubsub.topic(computationTopic);
|
|
89
|
+
const crypto = require('crypto');
|
|
90
|
+
|
|
91
|
+
// Get manifest to resolve dependencies
|
|
92
|
+
const manifest = getManifest(config.activeProductLines || [], calculations);
|
|
93
|
+
|
|
94
|
+
// Resolve dependency chain
|
|
95
|
+
const dependencyPasses = resolveDependencyChain(targetComputation, manifest);
|
|
96
|
+
|
|
97
|
+
logger.log('INFO', `[On-Demand] Resolved dependency chain for ${targetComputation}:`, {
|
|
98
|
+
totalPasses: dependencyPasses.length,
|
|
99
|
+
passes: dependencyPasses.map(p => `Pass ${p.pass}: ${p.computations.length} computations`)
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const triggeredMessages = [];
|
|
103
|
+
|
|
104
|
+
// Trigger each pass in order
|
|
105
|
+
for (const passGroup of dependencyPasses) {
|
|
106
|
+
const { pass, computations } = passGroup;
|
|
107
|
+
|
|
108
|
+
// Trigger all computations in this pass
|
|
109
|
+
for (const computation of computations) {
|
|
110
|
+
const dispatchId = crypto.randomUUID();
|
|
111
|
+
const isTarget = normalizeName(computation) === normalizeName(targetComputation);
|
|
112
|
+
|
|
113
|
+
const computationMessage = {
|
|
114
|
+
action: 'RUN_COMPUTATION_DATE',
|
|
115
|
+
computation: computation,
|
|
116
|
+
date: date,
|
|
117
|
+
pass: String(pass),
|
|
118
|
+
dispatchId: dispatchId,
|
|
119
|
+
triggerReason: metadata.triggerReason || 'on_demand',
|
|
120
|
+
resources: metadata.resources || 'standard',
|
|
121
|
+
metadata: {
|
|
122
|
+
...metadata,
|
|
123
|
+
onDemand: true,
|
|
124
|
+
isTargetComputation: isTarget,
|
|
125
|
+
targetCid: metadata.targetCid || null // Pass through targetCid for optimization
|
|
126
|
+
},
|
|
127
|
+
traceContext: {
|
|
128
|
+
traceId: crypto.randomBytes(16).toString('hex'),
|
|
129
|
+
spanId: crypto.randomBytes(8).toString('hex'),
|
|
130
|
+
sampled: true
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
await topic.publishMessage({
|
|
135
|
+
data: Buffer.from(JSON.stringify(computationMessage))
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
triggeredMessages.push(computationMessage);
|
|
139
|
+
|
|
140
|
+
logger.log('INFO', `[On-Demand] Triggered ${computation} (Pass ${pass})${isTarget ? ' [TARGET]' : ' [DEPENDENCY]'}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return triggeredMessages;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
resolveDependencyChain,
|
|
149
|
+
triggerComputationWithDependencies
|
|
150
|
+
};
|
|
151
|
+
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
# Data Feeder Pipeline (
|
|
2
|
-
#
|
|
1
|
+
# Data Feeder Pipeline (V3.1 - Syntax Fixed)
|
|
2
|
+
# Starts at 22:00 UTC via Cloud Scheduler.
|
|
3
|
+
# Fixes: Split assign/call steps and corrected assign syntax.
|
|
3
4
|
|
|
4
5
|
main:
|
|
5
6
|
params: [input]
|
|
@@ -8,17 +9,8 @@ main:
|
|
|
8
9
|
assign:
|
|
9
10
|
- project: '${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}'
|
|
10
11
|
- location: "europe-west1"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- default_retry:
|
|
14
|
-
predicate: ${http.default_retry_predicate}
|
|
15
|
-
max_retries: 5
|
|
16
|
-
backoff:
|
|
17
|
-
initial_delay: 2
|
|
18
|
-
max_delay: 60
|
|
19
|
-
multiplier: 2
|
|
20
|
-
|
|
21
|
-
# --- TEST MODE / SELECTIVE EXECUTION ---
|
|
12
|
+
|
|
13
|
+
# --- TEST MODE ---
|
|
22
14
|
- check_test_mode:
|
|
23
15
|
switch:
|
|
24
16
|
- condition: '${input != null and "target_step" in input}'
|
|
@@ -26,130 +18,164 @@ main:
|
|
|
26
18
|
- route_test:
|
|
27
19
|
switch:
|
|
28
20
|
- condition: '${input.target_step == "market"}'
|
|
29
|
-
next:
|
|
30
|
-
- condition: '${input.target_step == "
|
|
31
|
-
next:
|
|
21
|
+
next: phase_2200_price
|
|
22
|
+
- condition: '${input.target_step == "midnight"}'
|
|
23
|
+
next: phase_0000_rankings
|
|
32
24
|
- condition: '${input.target_step == "social"}'
|
|
33
|
-
next:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
branches:
|
|
41
|
-
- price_fetch:
|
|
42
|
-
steps:
|
|
43
|
-
- call_price_fetcher:
|
|
44
|
-
try:
|
|
45
|
-
call: http.post
|
|
46
|
-
args:
|
|
47
|
-
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/price-fetcher"}'
|
|
48
|
-
auth: { type: OIDC }
|
|
49
|
-
timeout: 300
|
|
50
|
-
retry: ${default_retry} # Fixed: Moved retry to a Try Step
|
|
51
|
-
- insights_fetch:
|
|
52
|
-
steps:
|
|
53
|
-
- call_insights_fetcher:
|
|
54
|
-
try:
|
|
55
|
-
call: http.post
|
|
56
|
-
args:
|
|
57
|
-
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/insights-fetcher"}'
|
|
58
|
-
auth: { type: OIDC }
|
|
59
|
-
timeout: 300
|
|
60
|
-
retry: ${default_retry} # Fixed: Moved retry to a Try Step
|
|
61
|
-
|
|
62
|
-
- index_market_data:
|
|
25
|
+
next: social_loop_start
|
|
26
|
+
|
|
27
|
+
# ==========================================
|
|
28
|
+
# PHASE 1: MARKET CLOSE (Starts 22:00 UTC)
|
|
29
|
+
# ==========================================
|
|
30
|
+
|
|
31
|
+
- phase_2200_price:
|
|
63
32
|
try:
|
|
64
33
|
call: http.post
|
|
65
34
|
args:
|
|
66
|
-
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/
|
|
67
|
-
body:
|
|
68
|
-
targetDate: '${market_date}'
|
|
35
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/price-fetcher"}'
|
|
69
36
|
auth: { type: OIDC }
|
|
70
37
|
timeout: 300
|
|
71
|
-
|
|
38
|
+
except:
|
|
39
|
+
as: e
|
|
40
|
+
steps:
|
|
41
|
+
- log_price_error:
|
|
42
|
+
call: sys.log
|
|
43
|
+
args: { severity: "WARNING", text: "Price fetch timed out/failed. Proceeding." }
|
|
44
|
+
|
|
45
|
+
- wait_10_after_price:
|
|
46
|
+
call: sys.sleep
|
|
47
|
+
args: { seconds: 600 } # 10 Minutes
|
|
72
48
|
|
|
73
|
-
#
|
|
74
|
-
-
|
|
49
|
+
# FIX 1: Split assign and call
|
|
50
|
+
- prepare_index_price:
|
|
75
51
|
assign:
|
|
76
|
-
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
- do_midnight_sleep:
|
|
80
|
-
call: sys.sleep
|
|
52
|
+
- today: '${text.split(time.format(sys.now()), "T")[0]}'
|
|
53
|
+
- index_today_after_price:
|
|
54
|
+
call: http.post
|
|
81
55
|
args:
|
|
82
|
-
|
|
56
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
|
|
57
|
+
body: { targetDate: '${today}' }
|
|
58
|
+
auth: { type: OIDC }
|
|
83
59
|
|
|
84
|
-
|
|
85
|
-
|
|
60
|
+
- wait_10_before_insights:
|
|
61
|
+
call: sys.sleep
|
|
62
|
+
args: { seconds: 600 }
|
|
63
|
+
|
|
64
|
+
- phase_2200_insights:
|
|
86
65
|
try:
|
|
87
66
|
call: http.post
|
|
88
67
|
args:
|
|
89
|
-
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/
|
|
68
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/insights-fetcher"}'
|
|
90
69
|
auth: { type: OIDC }
|
|
91
70
|
timeout: 300
|
|
92
|
-
retry: ${default_retry}
|
|
93
71
|
except:
|
|
94
72
|
as: e
|
|
95
73
|
steps:
|
|
96
|
-
-
|
|
74
|
+
- log_insights_error:
|
|
97
75
|
call: sys.log
|
|
98
|
-
args:
|
|
99
|
-
severity: "ERROR"
|
|
100
|
-
text: '${"Rankings Fetch Failed: " + json.encode(e)}'
|
|
76
|
+
args: { severity: "WARNING", text: "Insights fetch timed out/failed. Proceeding." }
|
|
101
77
|
|
|
102
|
-
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
args:
|
|
106
|
-
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
|
|
107
|
-
auth: { type: OIDC }
|
|
108
|
-
timeout: 300
|
|
109
|
-
retry: ${default_retry}
|
|
78
|
+
- wait_10_after_insights:
|
|
79
|
+
call: sys.sleep
|
|
80
|
+
args: { seconds: 600 }
|
|
110
81
|
|
|
111
|
-
|
|
82
|
+
# FIX 2: Split assign and call
|
|
83
|
+
- prepare_index_insights:
|
|
112
84
|
assign:
|
|
113
|
-
|
|
114
|
-
-
|
|
85
|
+
- today: '${text.split(time.format(sys.now()), "T")[0]}'
|
|
86
|
+
- index_today_after_insights:
|
|
87
|
+
call: http.post
|
|
88
|
+
args:
|
|
89
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
|
|
90
|
+
body: { targetDate: '${today}' }
|
|
91
|
+
auth: { type: OIDC }
|
|
92
|
+
|
|
93
|
+
# ==========================================
|
|
94
|
+
# PHASE 2: WAIT FOR MIDNIGHT
|
|
95
|
+
# ==========================================
|
|
96
|
+
|
|
97
|
+
- align_to_midnight:
|
|
98
|
+
assign:
|
|
99
|
+
- now_sec: '${int(sys.now())}'
|
|
100
|
+
- day_sec: 86400
|
|
101
|
+
- sleep_midnight: '${day_sec - (now_sec % day_sec)}'
|
|
102
|
+
- wait_for_midnight:
|
|
103
|
+
call: sys.sleep
|
|
104
|
+
args: { seconds: '${sleep_midnight}' }
|
|
105
|
+
|
|
106
|
+
# ==========================================
|
|
107
|
+
# PHASE 3: MIDNIGHT TASKS (00:00 UTC)
|
|
108
|
+
# ==========================================
|
|
109
|
+
|
|
110
|
+
- phase_0000_rankings:
|
|
115
111
|
try:
|
|
116
112
|
call: http.post
|
|
117
113
|
args:
|
|
118
|
-
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/
|
|
119
|
-
body:
|
|
120
|
-
targetDate: '${current_date}'
|
|
114
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/fetch-popular-investors"}'
|
|
121
115
|
auth: { type: OIDC }
|
|
122
116
|
timeout: 300
|
|
123
|
-
|
|
117
|
+
except:
|
|
118
|
+
as: e
|
|
119
|
+
steps:
|
|
120
|
+
- log_ranking_error:
|
|
121
|
+
call: sys.log
|
|
122
|
+
args: { severity: "WARNING", text: "Rankings failed. Proceeding to Social (risky)." }
|
|
124
123
|
|
|
125
|
-
-
|
|
124
|
+
- wait_10_after_rankings:
|
|
125
|
+
call: sys.sleep
|
|
126
|
+
args: { seconds: 600 }
|
|
127
|
+
|
|
128
|
+
# FIX 3: Split assign and call
|
|
129
|
+
- prepare_index_rankings:
|
|
130
|
+
assign:
|
|
131
|
+
- today: '${text.split(time.format(sys.now()), "T")[0]}'
|
|
132
|
+
- index_today_after_rankings:
|
|
133
|
+
call: http.post
|
|
134
|
+
args:
|
|
135
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
|
|
136
|
+
body: { targetDate: '${today}' }
|
|
137
|
+
auth: { type: OIDC }
|
|
138
|
+
|
|
139
|
+
- phase_0000_social:
|
|
126
140
|
try:
|
|
127
141
|
call: http.post
|
|
128
142
|
args:
|
|
129
|
-
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/
|
|
143
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
|
|
130
144
|
auth: { type: OIDC }
|
|
131
145
|
timeout: 300
|
|
132
|
-
|
|
146
|
+
except:
|
|
147
|
+
as: e
|
|
148
|
+
steps:
|
|
149
|
+
- log_social_error:
|
|
150
|
+
call: sys.log
|
|
151
|
+
args: { severity: "WARNING", text: "Social failed. Proceeding." }
|
|
152
|
+
|
|
153
|
+
- wait_10_after_social:
|
|
154
|
+
call: sys.sleep
|
|
155
|
+
args: { seconds: 600 }
|
|
156
|
+
|
|
157
|
+
- global_index_midnight:
|
|
158
|
+
call: http.post
|
|
159
|
+
args:
|
|
160
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
|
|
161
|
+
# No targetDate = Global Run
|
|
162
|
+
auth: { type: OIDC }
|
|
163
|
+
|
|
164
|
+
# ==========================================
|
|
165
|
+
# PHASE 4: SOCIAL LOOP (Every 3 Hours)
|
|
166
|
+
# ==========================================
|
|
133
167
|
|
|
134
|
-
# --- PHASE 4: RECURRING SOCIAL FETCH (UTC Aligned 3hr) ---
|
|
135
168
|
- init_social_loop:
|
|
136
169
|
assign:
|
|
137
170
|
- i: 0
|
|
138
171
|
|
|
139
|
-
-
|
|
172
|
+
- social_loop_start:
|
|
140
173
|
switch:
|
|
141
|
-
- condition: ${i < 7}
|
|
174
|
+
- condition: ${i < 7} # Covers the remainder of the 24h cycle
|
|
142
175
|
steps:
|
|
143
|
-
-
|
|
144
|
-
assign:
|
|
145
|
-
- now_sec_loop: '${int(sys.now())}'
|
|
146
|
-
- window_size: 10800
|
|
147
|
-
- sleep_loop: '${window_size - (now_sec_loop % window_size)}'
|
|
148
|
-
|
|
149
|
-
- wait_for_3hr_window:
|
|
176
|
+
- wait_3_hours:
|
|
150
177
|
call: sys.sleep
|
|
151
|
-
args:
|
|
152
|
-
seconds: '${sleep_loop}'
|
|
178
|
+
args: { seconds: 10800 }
|
|
153
179
|
|
|
154
180
|
- run_social_recurring:
|
|
155
181
|
try:
|
|
@@ -158,27 +184,34 @@ main:
|
|
|
158
184
|
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
|
|
159
185
|
auth: { type: OIDC }
|
|
160
186
|
timeout: 300
|
|
161
|
-
|
|
187
|
+
except:
|
|
188
|
+
as: e
|
|
189
|
+
steps:
|
|
190
|
+
- log_loop_social_warn:
|
|
191
|
+
call: sys.log
|
|
192
|
+
args: { severity: "WARNING", text: "Loop Social timed out. Proceeding." }
|
|
162
193
|
|
|
163
|
-
-
|
|
194
|
+
- wait_10_in_loop:
|
|
195
|
+
call: sys.sleep
|
|
196
|
+
args: { seconds: 600 }
|
|
197
|
+
|
|
198
|
+
# FIX 4: Split assign and call
|
|
199
|
+
- prepare_index_loop:
|
|
164
200
|
assign:
|
|
165
|
-
-
|
|
166
|
-
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
targetDate: '${cur_date_rec}'
|
|
173
|
-
auth: { type: OIDC }
|
|
174
|
-
timeout: 300
|
|
175
|
-
retry: ${default_retry}
|
|
201
|
+
- today: '${text.split(time.format(sys.now()), "T")[0]}'
|
|
202
|
+
- index_today_in_loop:
|
|
203
|
+
call: http.post
|
|
204
|
+
args:
|
|
205
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
|
|
206
|
+
body: { targetDate: '${today}' }
|
|
207
|
+
auth: { type: OIDC }
|
|
176
208
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
209
|
+
# FIX 5: Correct assign syntax (must be a list)
|
|
210
|
+
- increment_loop:
|
|
211
|
+
assign:
|
|
212
|
+
- i: '${i + 1}'
|
|
180
213
|
- next_iteration:
|
|
181
|
-
next:
|
|
214
|
+
next: social_loop_start
|
|
182
215
|
|
|
183
216
|
- finish:
|
|
184
|
-
return: "
|
|
217
|
+
return: "Complete 24h Cycle Finished"
|
|
@@ -94,6 +94,30 @@ exports.fetchAndStorePrices = async (config, dependencies) => {
|
|
|
94
94
|
|
|
95
95
|
logger.log('INFO', `[PriceFetcherHelpers] Wrote price date tracking document for ${today} with ${priceDatesArray.length} dates (from August 2025 onwards)`);
|
|
96
96
|
|
|
97
|
+
// Update root data indexer for today's date after price data is stored
|
|
98
|
+
try {
|
|
99
|
+
const { runRootDataIndexer } = require('../../root-data-indexer/index');
|
|
100
|
+
const rootDataIndexerConfig = config.rootDataIndexer || {
|
|
101
|
+
availabilityCollection: 'system_root_data_index',
|
|
102
|
+
earliestDate: '2025-08-01',
|
|
103
|
+
collections: {
|
|
104
|
+
prices: priceCollectionName
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const indexerConfig = {
|
|
109
|
+
...rootDataIndexerConfig,
|
|
110
|
+
targetDate: today // Index only today's date for speed
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
logger.log('INFO', `[PriceFetcherHelpers] Triggering root data indexer for date ${today} after price data storage...`);
|
|
114
|
+
await runRootDataIndexer(indexerConfig, dependencies);
|
|
115
|
+
logger.log('INFO', `[PriceFetcherHelpers] Root data indexer completed for date ${today}`);
|
|
116
|
+
} catch (indexerError) {
|
|
117
|
+
logger.log('ERROR', `[PriceFetcherHelpers] Failed to run root data indexer for ${today}`, indexerError);
|
|
118
|
+
// Continue - price data is stored, indexer failure is non-critical
|
|
119
|
+
}
|
|
120
|
+
|
|
97
121
|
const successMessage = `Successfully processed and saved daily prices for ${results.length} instruments to ${batchPromises.length} shards.`;
|
|
98
122
|
logger.log('SUCCESS', `[PriceFetcherHelpers] ${successMessage}`);
|
|
99
123
|
return { success: true, message: successMessage, instrumentsProcessed: results.length };
|