bulltrackers-module 1.0.194 → 1.0.195
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.
|
@@ -1,14 +1,41 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* FILENAME: CloudFunctions/NpmWrappers/bulltrackers-module/functions/task-engine/helpers/update_helpers.js
|
|
3
|
-
* (
|
|
4
|
-
* (
|
|
5
|
-
* (REFACTORED:
|
|
6
|
-
* (FIXED: Corrected variable name 'instId' to 'instrumentId' in final timestamp loops)
|
|
3
|
+
* (OPTIMIZED V2: Added "Circuit Breaker" for Proxy failures)
|
|
4
|
+
* (OPTIMIZED V2: Downgraded verbose per-user logs to TRACE to save costs)
|
|
5
|
+
* (REFACTORED: Concurrency set to 1, added fallback and verbose logging)
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
10
9
|
const pLimit = require('p-limit');
|
|
11
10
|
|
|
11
|
+
// --- CIRCUIT BREAKER STATE ---
|
|
12
|
+
// Persists across function invocations in the same instance.
|
|
13
|
+
// If the Proxy fails 3 times in a row, we stop trying it to save the 5s timeout cost.
|
|
14
|
+
let _consecutiveProxyFailures = 0;
|
|
15
|
+
const MAX_PROXY_FAILURES = 3;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Helper to check if we should attempt the proxy
|
|
19
|
+
*/
|
|
20
|
+
function shouldTryProxy() {
|
|
21
|
+
return _consecutiveProxyFailures < MAX_PROXY_FAILURES;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Helper to record proxy result
|
|
26
|
+
*/
|
|
27
|
+
function recordProxyOutcome(success) {
|
|
28
|
+
if (success) {
|
|
29
|
+
if (_consecutiveProxyFailures > 0) {
|
|
30
|
+
// Optional: Only log recovery to reduce noise
|
|
31
|
+
// console.log('[ProxyCircuit] Proxy recovered.');
|
|
32
|
+
}
|
|
33
|
+
_consecutiveProxyFailures = 0;
|
|
34
|
+
} else {
|
|
35
|
+
_consecutiveProxyFailures++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
12
39
|
/**
|
|
13
40
|
* (REFACTORED: Concurrency set to 1, added fallback and verbose logging)
|
|
14
41
|
*/
|
|
@@ -21,35 +48,65 @@ async function lookupUsernames(cids, { logger, headerManager, proxyManager }, co
|
|
|
21
48
|
const { USERNAME_LOOKUP_BATCH_SIZE, ETORO_API_RANKINGS_URL } = config;
|
|
22
49
|
const batches = [];
|
|
23
50
|
for (let i = 0; i < cids.length; i += USERNAME_LOOKUP_BATCH_SIZE) { batches.push(cids.slice(i, i + USERNAME_LOOKUP_BATCH_SIZE).map(Number)); }
|
|
24
|
-
|
|
25
|
-
|
|
51
|
+
|
|
52
|
+
const batchPromises = batches.map((batch, index) => limit(async () => {
|
|
53
|
+
const batchId = `batch-${index + 1}`;
|
|
54
|
+
logger.log('TRACE', `[lookupUsernames/${batchId}] Processing batch of ${batch.length} CIDs...`); // DOWNGRADED TO TRACE
|
|
55
|
+
|
|
26
56
|
const header = await headerManager.selectHeader();
|
|
27
57
|
if (!header) { logger.log('ERROR', `[lookupUsernames/${batchId}] Could not select a header.`); return null; }
|
|
58
|
+
|
|
28
59
|
let wasSuccess = false;
|
|
29
|
-
let proxyUsed =
|
|
60
|
+
let proxyUsed = false;
|
|
30
61
|
let response;
|
|
31
62
|
const url = `${ETORO_API_RANKINGS_URL}?Period=LastTwoYears`;
|
|
32
63
|
const options = { method: 'POST', headers: { ...header.header, 'Content-Type': 'application/json' }, body: JSON.stringify(batch) };
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
64
|
+
|
|
65
|
+
// --- 1. Try Proxy (Circuit Breaker Protected) ---
|
|
66
|
+
if (shouldTryProxy()) {
|
|
67
|
+
try {
|
|
68
|
+
logger.log('TRACE', `[lookupUsernames/${batchId}] Attempting fetch via AppScript proxy...`);
|
|
69
|
+
response = await proxyManager.fetch(url, options);
|
|
70
|
+
if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
|
|
71
|
+
|
|
72
|
+
wasSuccess = true;
|
|
73
|
+
proxyUsed = true;
|
|
74
|
+
recordProxyOutcome(true); // Reset failure count
|
|
75
|
+
logger.log('TRACE', `[lookupUsernames/${batchId}] AppScript proxy fetch successful.`); // DOWNGRADED TO TRACE
|
|
76
|
+
|
|
77
|
+
} catch (proxyError) {
|
|
78
|
+
recordProxyOutcome(false); // Increment failure count
|
|
79
|
+
logger.log('WARN', `[lookupUsernames/${batchId}] AppScript proxy fetch FAILED. Error: ${proxyError.message}. Failures: ${_consecutiveProxyFailures}/${MAX_PROXY_FAILURES}.`, { error: proxyError.message, source: 'AppScript' });
|
|
80
|
+
// Fall through to direct...
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
logger.log('TRACE', `[lookupUsernames/${batchId}] Circuit Breaker Open. Skipping Proxy.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- 2. Direct Fallback ---
|
|
87
|
+
if (!wasSuccess) {
|
|
88
|
+
try {
|
|
89
|
+
response = await fetch(url, options);
|
|
42
90
|
if (!response.ok) { const errorText = await response.text(); throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`); }
|
|
43
|
-
logger.log('
|
|
44
|
-
|
|
91
|
+
logger.log('TRACE', `[lookupUsernames/${batchId}] Direct node-fetch fallback successful.`); // DOWNGRADED TO TRACE
|
|
92
|
+
wasSuccess = true; // It worked eventually
|
|
93
|
+
} catch (fallbackError) {
|
|
94
|
+
logger.log('ERROR', `[lookupUsernames/${batchId}] Direct node-fetch fallback FAILED. Giving up on this batch.`, { error: fallbackError.message, source: 'eToro/Network' });
|
|
45
95
|
return null; // Give up on this batch
|
|
46
96
|
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (proxyUsed) { headerManager.updatePerformance(header.id, wasSuccess); }
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const data = await response.json(); return data;
|
|
103
|
+
} catch (parseError) {
|
|
104
|
+
logger.log('ERROR', `[lookupUsernames/${batchId}] Failed to parse JSON response.`, { error: parseError.message }); return null;
|
|
105
|
+
}
|
|
106
|
+
}));
|
|
50
107
|
|
|
51
108
|
const results = await Promise.allSettled(batchPromises);
|
|
52
|
-
const allUsers = results
|
|
109
|
+
const allUsers = results.filter(r => r.status === 'fulfilled' && r.value && Array.isArray(r.value)).flatMap(r => r.value);
|
|
53
110
|
logger.log('INFO', `[lookupUsernames] Found ${allUsers.length} public users out of ${cids.length}.`);
|
|
54
111
|
return allUsers;
|
|
55
112
|
}
|
|
@@ -64,118 +121,140 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
|
|
|
64
121
|
const today = new Date().toISOString().slice(0, 10);
|
|
65
122
|
const portfolioBlockId = `${Math.floor(parseInt(userId) / 1000000)}M`;
|
|
66
123
|
let isPrivate = false;
|
|
67
|
-
|
|
124
|
+
|
|
125
|
+
// DOWNGRADED TO TRACE
|
|
126
|
+
logger.log('TRACE', `[handleUpdate/${userId}] Starting update task. Type: ${userType}. Instruments: ${instrumentsToProcess.join(', ')}`);
|
|
68
127
|
|
|
69
128
|
// --- 1. Process History Fetch (Sequentially) ---
|
|
70
129
|
let historyHeader = null;
|
|
71
130
|
let wasHistorySuccess = false;
|
|
72
|
-
let proxyUsedForHistory =
|
|
131
|
+
let proxyUsedForHistory = false;
|
|
73
132
|
|
|
74
133
|
try {
|
|
75
134
|
if (!batchManager.checkAndSetHistoryFetched(userId)) {
|
|
76
|
-
logger.log('
|
|
135
|
+
logger.log('TRACE', `[handleUpdate/${userId}] Attempting history fetch.`);
|
|
77
136
|
historyHeader = await headerManager.selectHeader();
|
|
78
|
-
if (!historyHeader) {
|
|
137
|
+
if (!historyHeader) {
|
|
138
|
+
logger.log('WARN', `[handleUpdate/${userId}] Could not select history header. Skipping history.`);
|
|
79
139
|
} else {
|
|
80
140
|
const historyUrl = `${config.ETORO_API_USERSTATS_URL}${username}/trades/oneYearAgo?CopyAsAsset=true`;
|
|
81
141
|
const options = { headers: historyHeader.header };
|
|
82
142
|
let response;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
proxyUsedForHistory = false;
|
|
91
|
-
try { response = await fetch(historyUrl, options);
|
|
92
|
-
if (!response.ok) { const errorText = await response.text();
|
|
93
|
-
throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`); } // SHIT we failed here too
|
|
94
|
-
wasHistorySuccess = true; // Fallback succeeded, we are so smart
|
|
143
|
+
|
|
144
|
+
// --- PROXY ATTEMPT ---
|
|
145
|
+
if (shouldTryProxy()) {
|
|
146
|
+
try {
|
|
147
|
+
logger.log('TRACE', `[handleUpdate/${userId}] Attempting history fetch via AppScript proxy...`);
|
|
148
|
+
response = await proxyManager.fetch(historyUrl, options);
|
|
149
|
+
if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
|
|
95
150
|
|
|
96
|
-
|
|
97
|
-
|
|
151
|
+
wasHistorySuccess = true;
|
|
152
|
+
proxyUsedForHistory = true;
|
|
153
|
+
recordProxyOutcome(true); // Reset
|
|
154
|
+
|
|
155
|
+
} catch (proxyError) {
|
|
156
|
+
recordProxyOutcome(false); // Count failure
|
|
157
|
+
logger.log('WARN', `[handleUpdate/${userId}] History fetch via AppScript proxy FAILED. Error: ${proxyError.message}. Failures: ${_consecutiveProxyFailures}/${MAX_PROXY_FAILURES}.`, { error: proxyError.message, source: 'AppScript' });
|
|
98
158
|
}
|
|
99
159
|
}
|
|
100
160
|
|
|
101
|
-
|
|
161
|
+
// --- DIRECT FALLBACK ---
|
|
162
|
+
if (!wasHistorySuccess) {
|
|
163
|
+
try {
|
|
164
|
+
response = await fetch(historyUrl, options);
|
|
165
|
+
if (!response.ok) { const errorText = await response.text(); throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`); }
|
|
166
|
+
wasHistorySuccess = true;
|
|
167
|
+
logger.log('TRACE', `[handleUpdate/${userId}] History fetch direct success.`);
|
|
168
|
+
} catch (fallbackError) {
|
|
169
|
+
logger.log('ERROR', `[handleUpdate/${userId}] History fetch direct fallback FAILED.`, { error: fallbackError.message, source: 'eToro/Network' });
|
|
170
|
+
wasHistorySuccess = false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (wasHistorySuccess) {
|
|
102
175
|
const data = await response.json();
|
|
103
|
-
await batchManager.addToTradingHistoryBatch(userId, portfolioBlockId, today, data, userType);
|
|
176
|
+
await batchManager.addToTradingHistoryBatch(userId, portfolioBlockId, today, data, userType);
|
|
177
|
+
}
|
|
104
178
|
}
|
|
105
|
-
} else {
|
|
106
|
-
|
|
107
|
-
|
|
179
|
+
} else {
|
|
180
|
+
logger.log('TRACE', `[handleUpdate/${userId}] History fetch skipped (already fetched).`);
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
logger.log('ERROR', `[handleUpdate/${userId}] Unhandled error during history processing.`, { error: err.message }); wasHistorySuccess = false;
|
|
184
|
+
} finally {
|
|
185
|
+
if (historyHeader && proxyUsedForHistory) { headerManager.updatePerformance(historyHeader.id, wasHistorySuccess); }
|
|
186
|
+
}
|
|
108
187
|
|
|
109
188
|
// --- 2. Process Portfolio Fetches (Sequentially) ---
|
|
110
|
-
logger.log('
|
|
189
|
+
logger.log('TRACE', `[handleUpdate/${userId}] Starting ${instrumentsToProcess.length} sequential portfolio fetches.`); // DOWNGRADED TO TRACE
|
|
111
190
|
|
|
112
191
|
for (const instId of instrumentsToProcess) {
|
|
113
192
|
if (isPrivate) {
|
|
114
|
-
logger.log('
|
|
193
|
+
logger.log('TRACE', `[handleUpdate/${userId}] Skipping remaining instruments (User Private).`);
|
|
115
194
|
break;
|
|
116
195
|
}
|
|
117
196
|
|
|
118
197
|
const portfolioHeader = await headerManager.selectHeader();
|
|
119
|
-
if (!portfolioHeader) { logger.log('ERROR', `[handleUpdate/${userId}] Could not select portfolio header for instId ${instId}. Skipping this instrument.`);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
198
|
+
if (!portfolioHeader) { logger.log('ERROR', `[handleUpdate/${userId}] Could not select portfolio header for instId ${instId}. Skipping this instrument.`); continue; }
|
|
122
199
|
|
|
123
200
|
const portfolioUrl = userType === 'speculator' ? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${instId}` : `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
|
|
124
|
-
|
|
125
201
|
const options = { headers: portfolioHeader.header };
|
|
126
202
|
let response;
|
|
127
203
|
let wasPortfolioSuccess = false;
|
|
128
|
-
let proxyUsedForPortfolio =
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
logger.log('TRACE', `[handleUpdate/${userId}] Attempting portfolio fetch for instId ${instId} via AppScript proxy...`);
|
|
133
|
-
response = await proxyManager.fetch(portfolioUrl, options);
|
|
134
|
-
if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`); // SHIT we failed here
|
|
135
|
-
wasPortfolioSuccess = true; // Oh we are smart, worked first time.
|
|
136
|
-
|
|
137
|
-
} catch (proxyError) { // try fallback with local node fetch using GCP IP Pools
|
|
138
|
-
logger.log('WARN', `[handleUpdate/${userId}] Portfolio fetch for instId ${instId} via AppScript proxy FAILED. Error: ${proxyError.message}. Attempting direct node-fetch fallback.`, { error: proxyError.message, source: 'AppScript' });
|
|
139
|
-
proxyUsedForPortfolio = false; // We are not using Appscript proxy here as fallback is GCP based, so false
|
|
140
|
-
|
|
204
|
+
let proxyUsedForPortfolio = false;
|
|
205
|
+
|
|
206
|
+
// --- PROXY ATTEMPT ---
|
|
207
|
+
if (shouldTryProxy()) {
|
|
141
208
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`); // SHIT we failed here
|
|
146
|
-
}
|
|
147
|
-
wasPortfolioSuccess = true; // Fallback succeeded we are so smart
|
|
209
|
+
logger.log('TRACE', `[handleUpdate/${userId}] Attempting portfolio fetch via AppScript proxy...`);
|
|
210
|
+
response = await proxyManager.fetch(portfolioUrl, options);
|
|
211
|
+
if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
|
|
148
212
|
|
|
213
|
+
wasPortfolioSuccess = true;
|
|
214
|
+
proxyUsedForPortfolio = true;
|
|
215
|
+
recordProxyOutcome(true); // Reset
|
|
216
|
+
|
|
217
|
+
} catch (proxyError) {
|
|
218
|
+
recordProxyOutcome(false); // Count failure
|
|
219
|
+
logger.log('WARN', `[handleUpdate/${userId}] Portfolio fetch via Proxy FAILED. Error: ${proxyError.message}. Failures: ${_consecutiveProxyFailures}/${MAX_PROXY_FAILURES}.`, { error: proxyError.message, source: 'AppScript' });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// --- DIRECT FALLBACK ---
|
|
224
|
+
if (!wasPortfolioSuccess) {
|
|
225
|
+
try {
|
|
226
|
+
response = await fetch(portfolioUrl, options);
|
|
227
|
+
if (!response.ok) { const errorText = await response.text(); throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`); }
|
|
228
|
+
wasPortfolioSuccess = true;
|
|
229
|
+
logger.log('TRACE', `[handleUpdate/${userId}] Portfolio fetch direct success.`);
|
|
149
230
|
} catch (fallbackError) {
|
|
150
|
-
logger.log('ERROR', `[handleUpdate/${userId}] Portfolio fetch
|
|
231
|
+
logger.log('ERROR', `[handleUpdate/${userId}] Portfolio fetch direct fallback FAILED.`, { error: fallbackError.message, source: 'eToro/Network' });
|
|
151
232
|
wasPortfolioSuccess = false;
|
|
152
233
|
}
|
|
153
234
|
}
|
|
154
235
|
|
|
155
|
-
// --- 4. Process Portfolio Result
|
|
236
|
+
// --- 4. Process Portfolio Result ---
|
|
156
237
|
if (wasPortfolioSuccess) {
|
|
157
238
|
const body = await response.text();
|
|
158
|
-
if (body.includes("user is PRIVATE")) { isPrivate = true; logger.log('WARN', `[handleUpdate/${userId}] User is PRIVATE. Marking for removal.`);
|
|
159
|
-
break; // Stop processing more portfolios for this private user
|
|
160
|
-
}
|
|
239
|
+
if (body.includes("user is PRIVATE")) { isPrivate = true; logger.log('WARN', `[handleUpdate/${userId}] User is PRIVATE. Marking for removal.`); break; }
|
|
161
240
|
|
|
162
241
|
try {
|
|
163
242
|
const portfolioJson = JSON.parse(body);
|
|
164
243
|
await batchManager.addToPortfolioBatch(userId, portfolioBlockId, today, portfolioJson, userType, instId);
|
|
165
|
-
|
|
166
|
-
} else { logger.log('INFO', `[handleUpdate/${userId}] Successfully processed full portfolio (normal user).`); } // Normal users
|
|
244
|
+
logger.log('TRACE', `[handleUpdate/${userId}] Portfolio processed successfully.`); // DOWNGRADED TO TRACE
|
|
167
245
|
|
|
168
|
-
} catch (parseError) {
|
|
169
|
-
wasPortfolioSuccess = false;
|
|
170
|
-
logger.log('ERROR', `[handleUpdate/${userId}] FAILED TO PARSE JSON RESPONSE
|
|
246
|
+
} catch (parseError) {
|
|
247
|
+
wasPortfolioSuccess = false;
|
|
248
|
+
logger.log('ERROR', `[handleUpdate/${userId}] FAILED TO PARSE JSON RESPONSE.`, { url: portfolioUrl, parseErrorMessage: parseError.message });
|
|
171
249
|
}
|
|
172
|
-
} else {
|
|
250
|
+
} else {
|
|
251
|
+
logger.log('WARN', `[handleUpdate/${userId}] Portfolio fetch failed for instId ${instId}.`);
|
|
252
|
+
}
|
|
173
253
|
|
|
174
254
|
if (proxyUsedForPortfolio) { headerManager.updatePerformance(portfolioHeader.id, wasPortfolioSuccess); }
|
|
175
255
|
}
|
|
176
256
|
|
|
177
257
|
// --- 5. Handle Private Users & Timestamps ---
|
|
178
|
-
// FIXED: Corrected variable naming here from 'instId' to 'instrumentId'
|
|
179
258
|
if (isPrivate) {
|
|
180
259
|
logger.log('WARN', `[handleUpdate/${userId}] Removing private user from updates.`);
|
|
181
260
|
for (const instrumentId of instrumentsToProcess) {
|
|
@@ -190,15 +269,13 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
|
|
|
190
269
|
}
|
|
191
270
|
|
|
192
271
|
// If not private, update all timestamps
|
|
193
|
-
// FIXED: Corrected variable naming here from 'instId' to 'instrumentId'
|
|
194
272
|
for (const instrumentId of instrumentsToProcess) {
|
|
195
273
|
await batchManager.updateUserTimestamp(userId, userType, instrumentId);
|
|
196
274
|
}
|
|
197
275
|
|
|
198
276
|
if (userType === 'speculator') { await batchManager.addSpeculatorTimestampFix(userId, String(Math.floor(userId/1e6)*1e6)); }
|
|
199
277
|
|
|
200
|
-
logger.log('
|
|
201
|
-
// 'finally' block for header flushing is handled by the main handler_creator.js
|
|
278
|
+
logger.log('TRACE', `[handleUpdate/${userId}] Update task finished successfully.`); // DOWNGRADED TO TRACE
|
|
202
279
|
}
|
|
203
280
|
|
|
204
281
|
module.exports = { handleUpdate, lookupUsernames };
|