bulltrackers-module 1.0.154 → 1.0.156
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/core/utils/intelligent_proxy_manager.js +39 -21
- package/functions/task-engine/helpers/update_helpers.js +98 -25
- package/functions/task-engine/utils/firestore_batch_manager.js +33 -0
- package/functions/task-engine/utils/task_engine_utils.js +57 -15
- package/index.js +42 -98
- package/package.json +1 -1
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
* It selects an available (unlocked) proxy for each request and locks it upon failure.
|
|
4
4
|
* * This module is designed to be reusable and receives all dependencies
|
|
5
5
|
* (firestore, logger) and configuration via its constructor.
|
|
6
|
+
* --- MODIFIED: Now includes exponential backoff and retries specifically for rate-limit errors. ---
|
|
6
7
|
*/
|
|
7
8
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
9
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
8
10
|
|
|
9
11
|
class IntelligentProxyManager {
|
|
10
12
|
/**
|
|
@@ -29,6 +31,8 @@ class IntelligentProxyManager {
|
|
|
29
31
|
this.proxyLockingEnabled = config.proxyLockingEnabled !== false;
|
|
30
32
|
this.proxies = {};
|
|
31
33
|
this.configLastLoaded = 0;
|
|
34
|
+
this.MAX_RETRIES = 3;
|
|
35
|
+
this.INITIAL_BACKOFF_MS = 1000;
|
|
32
36
|
if (this.proxyUrls.length === 0) { this.logger.log('WARN', '[ProxyManager] No proxy URLs provided in config.');
|
|
33
37
|
} else { const lockingStatus = this.proxyLockingEnabled ? "Locking Mechanism Enabled" : "Locking Mechanism DISABLED"; this.logger.log('INFO', `[ProxyManager] Initialized with ${this.proxyUrls.length} proxies and ${lockingStatus}.`); }
|
|
34
38
|
}
|
|
@@ -59,10 +63,8 @@ class IntelligentProxyManager {
|
|
|
59
63
|
*/
|
|
60
64
|
async _selectProxy() {
|
|
61
65
|
await this._loadConfig();
|
|
62
|
-
|
|
63
66
|
const availableProxies = this.proxyLockingEnabled ? Object.values(this.proxies).filter(p => p.status === 'unlocked') : Object.values(this.proxies);
|
|
64
|
-
if (availableProxies.length === 0) { const errorMsg = this.proxyLockingEnabled ? "All proxies are locked. No proxy available." : "No proxies are loaded. Cannot make request.";
|
|
65
|
-
this.logger.log('ERROR', `[ProxyManager] ${errorMsg}`); throw new Error(errorMsg); }
|
|
67
|
+
if (availableProxies.length === 0) { const errorMsg = this.proxyLockingEnabled ? "All proxies are locked. No proxy available." : "No proxies are loaded. Cannot make request."; this.logger.log('ERROR', `[ProxyManager] ${errorMsg}`); throw new Error(errorMsg); }
|
|
66
68
|
const selected = availableProxies[Math.floor(Math.random() * availableProxies.length)];
|
|
67
69
|
return { owner: selected.owner, url: selected.url };
|
|
68
70
|
}
|
|
@@ -75,28 +77,41 @@ class IntelligentProxyManager {
|
|
|
75
77
|
if (!this.proxyLockingEnabled) { this.logger.log('TRACE', `[ProxyManager] Locking skipped for ${owner} (locking is disabled).`); return; }
|
|
76
78
|
if (this.proxies[owner]) { this.proxies[owner].status = 'locked'; }
|
|
77
79
|
this.logger.log('WARN', `[ProxyManager] Locking proxy: ${owner}`);
|
|
78
|
-
try { const docRef = this.firestore.doc(this.PERFORMANCE_DOC_PATH);
|
|
79
|
-
await docRef.set({ locks: { [owner]: { locked: true, lastLocked: FieldValue.serverTimestamp() } } }, { merge: true });
|
|
80
|
+
try { const docRef = this.firestore.doc(this.PERFORMANCE_DOC_PATH); await docRef.set({ locks: { [owner]: { locked: true, lastLocked: FieldValue.serverTimestamp() } } }, { merge: true });
|
|
80
81
|
} catch (error) { this.logger.log('ERROR', `[ProxyManager] Failed to write lock for ${owner} to Firestore.`, { errorMessage: error.message }); }
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
/**
|
|
84
|
-
* Makes a fetch request
|
|
85
|
+
* --- MODIFIED: Makes a fetch request with exponential backoff for rate limits ---
|
|
85
86
|
* @param {string} targetUrl - The URL to fetch.
|
|
86
87
|
* @param {object} options - Fetch options (e.g., headers).
|
|
87
88
|
* @returns {Promise<object>} A mock Response object.
|
|
88
89
|
*/
|
|
89
90
|
async fetch(targetUrl, options = {}) {
|
|
90
91
|
let proxy = null;
|
|
91
|
-
try { proxy = await this._selectProxy();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
try { proxy = await this._selectProxy(); } catch (error) { return { ok: false, status: 503, error: { message: error.message }, headers: new Headers() }; }
|
|
93
|
+
let backoff = this.INITIAL_BACKOFF_MS;
|
|
94
|
+
let lastResponse = null;
|
|
95
|
+
for (let attempt = 1; attempt <= this.MAX_RETRIES; attempt++) {
|
|
96
|
+
const response = await this._fetchViaAppsScript(proxy.url, targetUrl, options);
|
|
97
|
+
lastResponse = response;
|
|
98
|
+
// 1. Success
|
|
99
|
+
if (response.ok) { return response; }
|
|
100
|
+
// 2. Rate Limit Error (Retryable)
|
|
101
|
+
if (response.isRateLimitError) { this.logger.log('WARN', `[ProxyManager] Rate limit hit on proxy ${proxy.owner} (Attempt ${attempt}/${this.MAX_RETRIES}). Backing off for ${backoff}ms...`, { url: targetUrl }); await sleep(backoff); backoff *= 2; continue; }
|
|
102
|
+
// 3. Other Fetch Error (Non-Retryable, Lock Proxy)
|
|
103
|
+
if (response.isUrlFetchError) { this.logger.log('ERROR', `[ProxyManager] Proxy ${proxy.owner} failed (non-rate-limit). Locking proxy.`, { url: targetUrl, status: response.status }); await this.lockProxy(proxy.owner); return response; }
|
|
104
|
+
// 4. Standard Error (e.g., 404, 500 from *target* URL, not proxy)
|
|
105
|
+
return response; }
|
|
106
|
+
// If loop finishes, all retries failed (likely all were rate-limit errors)
|
|
107
|
+
this.logger.log('ERROR', `[ProxyManager] Request failed after ${this.MAX_RETRIES} rate-limit retries.`, { url: targetUrl });
|
|
108
|
+
return lastResponse;
|
|
96
109
|
}
|
|
97
110
|
|
|
111
|
+
|
|
98
112
|
/**
|
|
99
113
|
* Internal function to call the Google AppScript proxy.
|
|
114
|
+
* --- MODIFIED: Now adds `isRateLimitError` flag to response ---
|
|
100
115
|
* @private
|
|
101
116
|
*/
|
|
102
117
|
async _fetchViaAppsScript(proxyUrl, targetUrl, options) {
|
|
@@ -106,19 +121,22 @@ class IntelligentProxyManager {
|
|
|
106
121
|
if (!response.ok) {
|
|
107
122
|
const errorText = await response.text();
|
|
108
123
|
this.logger.log('WARN', `[ProxyManager] Proxy infrastructure itself failed.`, { status: response.status, proxy: proxyUrl, error: errorText });
|
|
109
|
-
|
|
124
|
+
const isRateLimit = response.status === 429;
|
|
125
|
+
return { ok: false, status: response.status, isUrlFetchError: true, isRateLimitError: isRateLimit, error: { message: `Proxy infrastructure failed with status ${response.status}` }, headers: response.headers, text: () => Promise.resolve(errorText) }; }
|
|
110
126
|
const proxyResponse = await response.json();
|
|
111
127
|
if (proxyResponse.error) {
|
|
112
128
|
const errorMsg = proxyResponse.error.message || '';
|
|
113
|
-
if (errorMsg.toLowerCase().includes('service invoked too many times')) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
129
|
+
if (errorMsg.toLowerCase().includes('service invoked too many times')) { this.logger.log('WARN', `[ProxyManager] Proxy quota error: ${proxyUrl}`, { error: proxyResponse.error }); return { ok: false, status: 500, error: proxyResponse.error, isUrlFetchError: true, isRateLimitError: true, headers: new Headers() }; }
|
|
130
|
+
return { ok: false, status: 500, error: proxyResponse.error, isUrlFetchError: true, isRateLimitError: false, headers: new Headers(), text: () => Promise.resolve(errorMsg) }; }
|
|
131
|
+
return {
|
|
132
|
+
ok: proxyResponse.statusCode >= 200 && proxyResponse.statusCode < 300,
|
|
133
|
+
status: proxyResponse.statusCode,
|
|
134
|
+
headers: new Headers(proxyResponse.headers || {}),
|
|
135
|
+
json: () => Promise.resolve(JSON.parse(proxyResponse.body)),
|
|
136
|
+
text: () => Promise.resolve(proxyResponse.body),
|
|
137
|
+
isUrlFetchError: false,
|
|
138
|
+
isRateLimitError: false };
|
|
139
|
+
} catch (networkError) { this.logger.log('ERROR', `[ProxyManager] Network error calling proxy: ${proxyUrl}`, { errorMessage: networkError.message }); return { ok: false, status: 0, isUrlFetchError: true, isRateLimitError: false, error: { message: `Network error: ${networkError.message}` }, headers: new Headers() }; } }
|
|
122
140
|
}
|
|
123
141
|
|
|
124
142
|
module.exports = { IntelligentProxyManager };
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* (MODIFIED: To conditionally fetch history API once per user per batch)
|
|
4
4
|
* (MODIFIED: `lookupUsernames` runs batches in parallel)
|
|
5
5
|
* (MODIFIED: `handleUpdate` fetches history and all portfolios in parallel)
|
|
6
|
+
* (MODIFIED: `handleUpdate` now uses batchManager for history cache)
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
* --- MODIFIED: Conditionally fetches history only once per user per batch. ---
|
|
15
16
|
*/
|
|
16
17
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
17
|
-
const pLimit = require('p-limit');
|
|
18
|
+
const pLimit = require('p-limit');
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* (MODIFIED: Runs lookup batches in parallel)
|
|
@@ -22,26 +23,45 @@ const pLimit = require('p-limit'); // <--- IMPORT p-limit
|
|
|
22
23
|
async function lookupUsernames(cids, { logger, headerManager, proxyManager }, config) {
|
|
23
24
|
if (!cids?.length) return [];
|
|
24
25
|
logger.log('INFO', `[lookupUsernames] Looking up usernames for ${cids.length} CIDs.`);
|
|
26
|
+
|
|
27
|
+
// Use a new config value, falling back to 5
|
|
25
28
|
const limit = pLimit(config.USERNAME_LOOKUP_CONCURRENCY || 5);
|
|
26
29
|
const { USERNAME_LOOKUP_BATCH_SIZE, ETORO_API_RANKINGS_URL } = config;
|
|
30
|
+
|
|
27
31
|
const batches = [];
|
|
28
|
-
for (let i = 0; i < cids.length; i += USERNAME_LOOKUP_BATCH_SIZE) {
|
|
32
|
+
for (let i = 0; i < cids.length; i += USERNAME_LOOKUP_BATCH_SIZE) {
|
|
33
|
+
batches.push(cids.slice(i, i + USERNAME_LOOKUP_BATCH_SIZE).map(Number));
|
|
34
|
+
}
|
|
29
35
|
|
|
30
36
|
const batchPromises = batches.map(batch => limit(async () => {
|
|
31
37
|
const header = await headerManager.selectHeader();
|
|
32
|
-
if (!header) {
|
|
38
|
+
if (!header) {
|
|
39
|
+
logger.log('ERROR', '[lookupUsernames] Could not select a header.');
|
|
40
|
+
return null; // Return null to skip this batch
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
let success = false;
|
|
34
44
|
try {
|
|
35
45
|
const res = await proxyManager.fetch(`${ETORO_API_RANKINGS_URL}?Period=LastTwoYears`, { method: 'POST', headers: { ...header.header, 'Content-Type': 'application/json' }, body: JSON.stringify(batch) });
|
|
36
46
|
if (!res.ok) throw new Error(`API status ${res.status}`);
|
|
37
47
|
const data = await res.json();
|
|
38
48
|
success = true;
|
|
39
|
-
logger.log('DEBUG', 'Looked up usernames', { batch: batch.slice(0, 5) });
|
|
40
|
-
return data;
|
|
41
|
-
} catch (err) {
|
|
42
|
-
|
|
49
|
+
logger.log('DEBUG', 'Looked up usernames', { batch: batch.slice(0, 5) }); // Log only a few
|
|
50
|
+
return data; // Return data on success
|
|
51
|
+
} catch (err) {
|
|
52
|
+
logger.log('WARN', `[lookupUsernames] Failed batch`, { error: err.message });
|
|
53
|
+
return null; // Return null on failure
|
|
54
|
+
} finally {
|
|
55
|
+
headerManager.updatePerformance(header.id, success);
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
|
|
43
59
|
const results = await Promise.allSettled(batchPromises);
|
|
44
|
-
|
|
60
|
+
|
|
61
|
+
const allUsers = results
|
|
62
|
+
.filter(r => r.status === 'fulfilled' && r.value && Array.isArray(r.value))
|
|
63
|
+
.flatMap(r => r.value); // Flatten all successful batch results
|
|
64
|
+
|
|
45
65
|
logger.log('INFO', `[lookupUsernames] Found ${allUsers.length} public users out of ${cids.length}.`);
|
|
46
66
|
return allUsers;
|
|
47
67
|
}
|
|
@@ -49,8 +69,9 @@ async function lookupUsernames(cids, { logger, headerManager, proxyManager }, co
|
|
|
49
69
|
|
|
50
70
|
/**
|
|
51
71
|
* (MODIFIED: Fetches history and all portfolios in parallel)
|
|
72
|
+
* (MODIFIED: Uses batchManager for history cache)
|
|
52
73
|
*/
|
|
53
|
-
async function handleUpdate(task, taskId, { logger, headerManager, proxyManager, db, batchManager }, config, username
|
|
74
|
+
async function handleUpdate(task, taskId, { logger, headerManager, proxyManager, db, batchManager }, config, username) { // <--- REMOVED historyFetchedForUser
|
|
54
75
|
const { userId, instruments, instrumentId, userType } = task;
|
|
55
76
|
const instrumentsToProcess = userType === 'speculator' ? (instruments || [instrumentId]) : [undefined];
|
|
56
77
|
const today = new Date().toISOString().slice(0, 10);
|
|
@@ -63,59 +84,111 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
|
|
|
63
84
|
|
|
64
85
|
try {
|
|
65
86
|
// --- 1. Prepare History Fetch (if needed) ---
|
|
66
|
-
|
|
87
|
+
// (MODIFIED: Use batchManager's cross-invocation cache)
|
|
88
|
+
if (!batchManager.checkAndSetHistoryFetched(userId)) {
|
|
89
|
+
// This user has NOT been fetched in the last 10 mins (by this instance)
|
|
67
90
|
historyHeader = await headerManager.selectHeader();
|
|
68
|
-
if (historyHeader) {
|
|
91
|
+
if (historyHeader) {
|
|
92
|
+
// No need to add to a local set, batchManager did it.
|
|
69
93
|
const historyUrl = `${config.ETORO_API_USERSTATS_URL}${username}/trades/oneYearAgo?CopyAsAsset=true`;
|
|
70
|
-
historyFetchPromise = proxyManager.fetch(historyUrl, { headers: historyHeader.header });
|
|
94
|
+
historyFetchPromise = proxyManager.fetch(historyUrl, { headers: historyHeader.header });
|
|
95
|
+
} else {
|
|
96
|
+
logger.log('WARN', `[handleUpdate] Could not select history header for ${userId}. History will be skipped for this task.`);
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
logger.log('TRACE', `[handleUpdate] History fetch for ${userId} skipped (already fetched by this instance).`);
|
|
100
|
+
}
|
|
71
101
|
|
|
72
102
|
// --- 2. Prepare All Portfolio Fetches ---
|
|
73
103
|
const portfolioRequests = [];
|
|
74
104
|
for (const instId of instrumentsToProcess) {
|
|
75
105
|
const portfolioHeader = await headerManager.selectHeader();
|
|
76
106
|
if (!portfolioHeader) throw new Error(`Could not select portfolio header for ${userId}`);
|
|
77
|
-
|
|
78
|
-
|
|
107
|
+
|
|
108
|
+
const portfolioUrl = userType === 'speculator'
|
|
109
|
+
? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${instId}`
|
|
110
|
+
: `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
|
|
111
|
+
|
|
112
|
+
portfolioRequests.push({
|
|
113
|
+
instrumentId: instId,
|
|
114
|
+
url: portfolioUrl,
|
|
115
|
+
header: portfolioHeader,
|
|
116
|
+
promise: proxyManager.fetch(portfolioUrl, { headers: portfolioHeader.header })
|
|
117
|
+
});
|
|
118
|
+
}
|
|
79
119
|
|
|
80
120
|
// --- 3. Execute All API Calls in Parallel ---
|
|
81
|
-
const allPromises = [
|
|
121
|
+
const allPromises = [
|
|
122
|
+
...(historyFetchPromise ? [historyFetchPromise] : []),
|
|
123
|
+
...portfolioRequests.map(r => r.promise)
|
|
124
|
+
];
|
|
82
125
|
const allResults = await Promise.allSettled(allPromises);
|
|
83
126
|
|
|
84
127
|
// --- 4. Process History Result ---
|
|
85
128
|
let resultIndex = 0;
|
|
86
129
|
if (historyFetchPromise) {
|
|
87
130
|
const historyRes = allResults[resultIndex++];
|
|
88
|
-
if (historyRes.status === 'fulfilled' && historyRes.value.ok) {
|
|
131
|
+
if (historyRes.status === 'fulfilled' && historyRes.value.ok) {
|
|
132
|
+
const data = await historyRes.value.json();
|
|
133
|
+
wasHistorySuccess = true;
|
|
134
|
+
await batchManager.addToTradingHistoryBatch(userId, portfolioBlockId, today, data, userType);
|
|
135
|
+
} else {
|
|
136
|
+
logger.log('WARN', `[handleUpdate] History fetch failed for ${userId}`, { error: historyRes.reason || `status ${historyRes.value?.status}` });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
89
139
|
|
|
90
140
|
// --- 5. Process Portfolio Results ---
|
|
91
141
|
for (let i = 0; i < portfolioRequests.length; i++) {
|
|
92
142
|
const requestInfo = portfolioRequests[i];
|
|
93
143
|
const portfolioRes = allResults[resultIndex++];
|
|
94
144
|
let wasPortfolioSuccess = false;
|
|
145
|
+
|
|
95
146
|
if (portfolioRes.status === 'fulfilled' && portfolioRes.value.ok) {
|
|
96
147
|
const body = await portfolioRes.value.text();
|
|
97
|
-
if (body.includes("user is PRIVATE")) {
|
|
98
|
-
|
|
148
|
+
if (body.includes("user is PRIVATE")) {
|
|
149
|
+
isPrivate = true;
|
|
150
|
+
logger.log('WARN', `User ${userId} is private. Removing from updates.`);
|
|
151
|
+
break; // Stop processing more portfolios for this private user
|
|
152
|
+
} else {
|
|
153
|
+
wasPortfolioSuccess = true;
|
|
154
|
+
await batchManager.addToPortfolioBatch(userId, portfolioBlockId, today, JSON.parse(body), userType, requestInfo.instrumentId);
|
|
155
|
+
}
|
|
99
156
|
logger.log('DEBUG', 'Processing portfolio for user', { userId, portfolioUrl: requestInfo.url });
|
|
100
|
-
} else {
|
|
157
|
+
} else {
|
|
158
|
+
logger.log('WARN', `Failed to fetch portfolio`, { userId, url: requestInfo.url, error: portfolioRes.reason || `status ${portfolioRes.value?.status}` });
|
|
159
|
+
}
|
|
160
|
+
// Update performance for this specific header
|
|
101
161
|
headerManager.updatePerformance(requestInfo.header.id, wasPortfolioSuccess);
|
|
102
162
|
}
|
|
103
163
|
|
|
104
164
|
// --- 6. Handle Private Users & Timestamps ---
|
|
105
165
|
if (isPrivate) {
|
|
106
166
|
logger.log('WARN', `User ${userId} is private. Removing from updates.`);
|
|
107
|
-
for (const instrumentId of instrumentsToProcess) {
|
|
167
|
+
for (const instrumentId of instrumentsToProcess) {
|
|
168
|
+
await batchManager.deleteFromTimestampBatch(userId, userType, instrumentId);
|
|
169
|
+
}
|
|
108
170
|
const blockCountsRef = db.doc(config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS);
|
|
109
171
|
for (const instrumentId of instrumentsToProcess) {
|
|
110
172
|
const incrementField = `counts.${instrumentId}_${Math.floor(userId/1e6)*1e6}`;
|
|
111
|
-
|
|
112
|
-
|
|
173
|
+
// This is not batched, but it's a rare event.
|
|
174
|
+
await blockCountsRef.set({ [incrementField]: FieldValue.increment(-1) }, { merge: true });
|
|
175
|
+
}
|
|
176
|
+
return; // Don't update timestamps
|
|
113
177
|
}
|
|
114
178
|
|
|
115
|
-
|
|
116
|
-
|
|
179
|
+
// If not private, update all timestamps
|
|
180
|
+
for (const instrumentId of instrumentsToProcess) {
|
|
181
|
+
await batchManager.updateUserTimestamp(userId, userType, instrumentId);
|
|
182
|
+
}
|
|
183
|
+
if (userType === 'speculator') {
|
|
184
|
+
await batchManager.addSpeculatorTimestampFix(userId, String(Math.floor(userId/1e6)*1e6));
|
|
185
|
+
}
|
|
117
186
|
|
|
118
|
-
} finally {
|
|
187
|
+
} finally {
|
|
188
|
+
if (historyHeader) { // historyHeader is only set if a fetch was attempted
|
|
189
|
+
headerManager.updatePerformance(historyHeader.id, wasHistorySuccess);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
119
192
|
}
|
|
120
193
|
|
|
121
194
|
module.exports = { handleUpdate, lookupUsernames };
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* REFACTORED: Renamed 'firestore' to 'db' for consistency.
|
|
4
4
|
* OPTIMIZED: Added logic to handle speculator timestamp fixes within the batch.
|
|
5
5
|
* --- MODIFIED: Added username map caching and trading history batching. ---
|
|
6
|
+
* --- MODIFIED: Added cross-invocation cache for history fetches. ---
|
|
6
7
|
*/
|
|
7
8
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
8
9
|
|
|
@@ -16,9 +17,18 @@ class FirestoreBatchManager {
|
|
|
16
17
|
this.timestampBatch = {};
|
|
17
18
|
this.tradingHistoryBatch = {};
|
|
18
19
|
this.speculatorTimestampFixBatch = {};
|
|
20
|
+
|
|
21
|
+
// Username map cache
|
|
19
22
|
this.usernameMap = new Map();
|
|
20
23
|
this.usernameMapUpdates = {};
|
|
21
24
|
this.usernameMapLastLoaded = 0;
|
|
25
|
+
|
|
26
|
+
// History fetch cache (NEW)
|
|
27
|
+
this.historyFetchedUserIds = new Set();
|
|
28
|
+
this.historyCacheTimestamp = Date.now();
|
|
29
|
+
// Set a 10-minute TTL on this cache (600,000 ms)
|
|
30
|
+
this.HISTORY_CACHE_TTL_MS = config.HISTORY_CACHE_TTL_MS || 600000;
|
|
31
|
+
|
|
22
32
|
this.processedSpeculatorCids = new Set();
|
|
23
33
|
this.usernameMapCollectionName = config.FIRESTORE_COLLECTION_USERNAME_MAP;
|
|
24
34
|
this.normalHistoryCollectionName = config.FIRESTORE_COLLECTION_NORMAL_HISTORY;
|
|
@@ -27,6 +37,29 @@ class FirestoreBatchManager {
|
|
|
27
37
|
logger.log('INFO', 'FirestoreBatchManager initialized.');
|
|
28
38
|
}
|
|
29
39
|
|
|
40
|
+
/*
|
|
41
|
+
* NEW: Checks if a user's history has been fetched in the last 10 minutes.
|
|
42
|
+
* If not, it logs them as fetched and returns false (to trigger a fetch).
|
|
43
|
+
* @param {string} userId
|
|
44
|
+
* @returns {boolean} True if already fetched, false if not.
|
|
45
|
+
*/
|
|
46
|
+
checkAndSetHistoryFetched(userId) {
|
|
47
|
+
// Check if the cache is stale
|
|
48
|
+
if (Date.now() - this.historyCacheTimestamp > this.HISTORY_CACHE_TTL_MS) {
|
|
49
|
+
this.logger.log('INFO', '[BATCH] History fetch cache (10m TTL) expired. Clearing set.');
|
|
50
|
+
this.historyFetchedUserIds.clear();
|
|
51
|
+
this.historyCacheTimestamp = Date.now();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (this.historyFetchedUserIds.has(userId)) {
|
|
55
|
+
return true; // Yes, already fetched
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Not fetched yet. Mark as fetched and return false.
|
|
59
|
+
this.historyFetchedUserIds.add(userId);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
30
63
|
_getUsernameShardId(cid) { return `cid_map_shard_${Math.floor(parseInt(cid) / 10000) % 10}`; }
|
|
31
64
|
|
|
32
65
|
// _scheduleFlush() { if (!this.batchTimeout) this.batchTimeout = setTimeout(() => this.flushBatches(), this.config.TASK_ENGINE_FLUSH_INTERVAL_MS); } Old version
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* FILENAME: CloudFunctions/NpmWrappers/bulltrackers-module/functions/task-engine/utils/task_engine_utils.js
|
|
3
3
|
* (MODIFIED: To pass down a Set to track history fetches)
|
|
4
4
|
* (MODIFIED: To run all update tasks in parallel with a concurrency limit)
|
|
5
|
+
* (MODIFIED: To use a SINGLE parallel work pool for ALL tasks)
|
|
6
|
+
* (MODIFIED: To remove local history cache set)
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -13,7 +15,7 @@
|
|
|
13
15
|
const { handleDiscover } = require('../helpers/discover_helpers');
|
|
14
16
|
const { handleVerify } = require('../helpers/verify_helpers');
|
|
15
17
|
const { handleUpdate, lookupUsernames } = require('../helpers/update_helpers');
|
|
16
|
-
const pLimit = require('p-limit');
|
|
18
|
+
const pLimit = require('p-limit'); // <--- IMPORT p-limit
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Parses Pub/Sub message into task array.
|
|
@@ -45,32 +47,72 @@ async function prepareTaskBatches(tasks, batchManager, logger) {
|
|
|
45
47
|
async function runUsernameLookups(tasksToRun, cidsToLookup, dependencies, config, batchManager, logger) {
|
|
46
48
|
if (!cidsToLookup.size) return;
|
|
47
49
|
logger.log('INFO', `[TaskEngine] Looking up ${cidsToLookup.size} usernames...`);
|
|
48
|
-
|
|
50
|
+
// Pass config to lookupUsernames
|
|
51
|
+
const foundUsers = await lookupUsernames([...cidsToLookup.keys()], dependencies, config); // <--- PASS FULL CONFIG
|
|
49
52
|
for (const u of foundUsers) { const cid = String(u.CID), username = u.Value.UserName; batchManager.addUsernameMapUpdate(cid, username); const task = cidsToLookup.get(cid); if (task) { tasksToRun.push({ task, username }); cidsToLookup.delete(cid); } }
|
|
50
53
|
if (cidsToLookup.size) logger.log('WARN', `[TaskEngine] Could not find ${cidsToLookup.size} usernames (likely private).`, { skippedCids: [...cidsToLookup.keys()] });
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
/**
|
|
54
57
|
* Executes all tasks.
|
|
58
|
+
* (MODIFIED: Runs ALL tasks in a single parallel pool)
|
|
55
59
|
*/
|
|
56
60
|
async function executeTasks(tasksToRun, otherTasks, dependencies, config, taskId) {
|
|
57
|
-
const { logger } = dependencies;
|
|
58
|
-
|
|
61
|
+
const { logger, batchManager } = dependencies; // <--- Get batchManager
|
|
62
|
+
|
|
63
|
+
// REMOVED: const historyFetchedForUser = new Set();
|
|
64
|
+
|
|
65
|
+
// Create one unified parallel pool
|
|
66
|
+
const limit = pLimit(config.TASK_ENGINE_CONCURRENCY || 10);
|
|
67
|
+
const allTaskPromises = [];
|
|
68
|
+
let taskCounters = { update: 0, discover: 0, verify: 0, unknown: 0, failed: 0 };
|
|
69
|
+
|
|
70
|
+
// 1. Queue 'other' tasks (discover, verify)
|
|
59
71
|
for (const task of otherTasks) {
|
|
60
72
|
const subTaskId = `${task.type}-${task.userType || 'unknown'}-${task.userId || task.cids?.[0] || 'sub'}`;
|
|
61
73
|
const handler = { discover: handleDiscover, verify: handleVerify }[task.type];
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
|
|
75
|
+
if (handler) {
|
|
76
|
+
allTaskPromises.push(
|
|
77
|
+
limit(() =>
|
|
78
|
+
handler(task, subTaskId, dependencies, config)
|
|
79
|
+
.then(() => taskCounters[task.type]++)
|
|
80
|
+
.catch(err => {
|
|
81
|
+
logger.log('ERROR', `[TaskEngine/${taskId}] Error in ${task.type} for ${subTaskId}`, { errorMessage: err.message });
|
|
82
|
+
taskCounters.failed++;
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
} else {
|
|
87
|
+
logger.log('ERROR', `[TaskEngine/${taskId}] Unknown task type: ${task.type}`);
|
|
88
|
+
taskCounters.unknown++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 2. Queue 'update' tasks
|
|
93
|
+
for (const { task, username } of tasksToRun) {
|
|
69
94
|
const subTaskId = `${task.type}-${task.userType || 'unknown'}-${task.userId}`;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
95
|
+
allTaskPromises.push(
|
|
96
|
+
limit(() =>
|
|
97
|
+
// Pass batchManager instead of the local set
|
|
98
|
+
handleUpdate(task, subTaskId, dependencies, config, username) // <--- REMOVED historyFetchedForUser
|
|
99
|
+
.then(() => taskCounters.update++)
|
|
100
|
+
.catch(err => {
|
|
101
|
+
logger.log('ERROR', `[TaskEngine/${taskId}] Error in handleUpdate for ${task.userId}`, { errorMessage: err.message });
|
|
102
|
+
taskCounters.failed++;
|
|
103
|
+
})
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 3. Wait for ALL tasks to complete
|
|
109
|
+
await Promise.all(allTaskPromises);
|
|
110
|
+
|
|
111
|
+
// 4. Log final summary
|
|
112
|
+
logger.log(
|
|
113
|
+
taskCounters.failed > 0 ? 'WARN' : 'SUCCESS',
|
|
114
|
+
`[TaskEngine/${taskId}] Processed all tasks. Updates: ${taskCounters.update}, Discovers: ${taskCounters.discover}, Verifies: ${taskCounters.verify}, Unknown: ${taskCounters.unknown}, Failed: ${taskCounters.failed}.`
|
|
115
|
+
);
|
|
74
116
|
}
|
|
75
117
|
|
|
76
118
|
module.exports = { parseTaskPayload, prepareTaskBatches, runUsernameLookups, executeTasks };
|
package/index.js
CHANGED
|
@@ -1,101 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Main entry point for the Bulltrackers shared module.
|
|
3
|
-
*
|
|
4
|
-
* to enforce a clear naming convention and dependency injection pattern.
|
|
3
|
+
* Export the pipes!
|
|
5
4
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
handleDiscover : require('./functions/task-engine/helpers/discover_helpers').handleDiscover,
|
|
48
|
-
handleVerify : require('./functions/task-engine/helpers/verify_helpers').handleVerify,
|
|
49
|
-
handleUpdate : require('./functions/task-engine/helpers/update_helpers').handleUpdate,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// --- Pipe 4: Computation System ---
|
|
54
|
-
|
|
55
|
-
const computationSystem = {
|
|
56
|
-
runComputationPass : require('./functions/computation-system/helpers/computation_pass_runner').runComputationPass,
|
|
57
|
-
dataLoader : require('./functions/computation-system/utils/data_loader'),
|
|
58
|
-
computationUtils : require('./functions/computation-system/utils/utils'),
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// --- Pipe 5: API ---
|
|
63
|
-
|
|
64
|
-
const api = {
|
|
65
|
-
createApiApp : require('./functions/generic-api/index').createApiApp,
|
|
66
|
-
helpers : require('./functions/generic-api/helpers/api_helpers'),
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// --- Pipe 6: Maintenance ---
|
|
71
|
-
|
|
72
|
-
const maintenance = {
|
|
73
|
-
runSpeculatorCleanup : require('./functions/speculator-cleanup-orchestrator/helpers/cleanup_helpers') .runCleanup,
|
|
74
|
-
handleInvalidSpeculator : require('./functions/invalid-speculator-handler/helpers/handler_helpers') .handleInvalidSpeculator,
|
|
75
|
-
runFetchInsights : require('./functions/fetch-insights/helpers/handler_helpers').fetchAndStoreInsights,
|
|
76
|
-
runFetchPrices : require('./functions/etoro-price-fetcher/helpers/handler_helpers').fetchAndStorePrices,
|
|
77
|
-
runSocialOrchestrator : require('./functions/social-orchestrator/helpers/orchestrator_helpers') .runSocialOrchestrator,
|
|
78
|
-
handleSocialTask : require('./functions/social-task-handler/helpers/handler_helpers') .handleSocialTask,
|
|
79
|
-
runBackfillAssetPrices : require('./functions/price-backfill/helpers/handler_helpers') .runBackfillAssetPrices,
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// --- Pipe 7: Proxy ---
|
|
84
|
-
|
|
85
|
-
const proxy = {
|
|
86
|
-
handlePost : require('./functions/appscript-api/index').handlePost,
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
module.exports = {
|
|
91
|
-
pipe: {
|
|
92
|
-
core,
|
|
93
|
-
orchestrator,
|
|
94
|
-
dispatcher,
|
|
95
|
-
taskEngine,
|
|
96
|
-
computationSystem,
|
|
97
|
-
api,
|
|
98
|
-
maintenance,
|
|
99
|
-
proxy,
|
|
100
|
-
}
|
|
101
|
-
};
|
|
5
|
+
// Core
|
|
6
|
+
const core = { IntelligentHeaderManager : require('./functions/core/utils/intelligent_header_manager') .IntelligentHeaderManager,
|
|
7
|
+
IntelligentProxyManager : require('./functions/core/utils/intelligent_proxy_manager') .IntelligentProxyManager,
|
|
8
|
+
FirestoreBatchManager : require('./functions/task-engine/utils/firestore_batch_manager') .FirestoreBatchManager,
|
|
9
|
+
firestoreUtils : require('./functions/core/utils/firestore_utils'),
|
|
10
|
+
pubsubUtils : require('./functions/core/utils/pubsub_utils') };
|
|
11
|
+
// Orchestrator
|
|
12
|
+
const orchestrator = { runDiscoveryOrchestrator : require('./functions/orchestrator/index') .runDiscoveryOrchestrator,
|
|
13
|
+
runUpdateOrchestrator : require('./functions/orchestrator/index') .runUpdateOrchestrator,
|
|
14
|
+
checkDiscoveryNeed : require('./functions/orchestrator/helpers/discovery_helpers') .checkDiscoveryNeed,
|
|
15
|
+
getDiscoveryCandidates : require('./functions/orchestrator/helpers/discovery_helpers') .getDiscoveryCandidates,
|
|
16
|
+
dispatchDiscovery : require('./functions/orchestrator/helpers/discovery_helpers') .dispatchDiscovery,
|
|
17
|
+
getUpdateTargets : require('./functions/orchestrator/helpers/update_helpers') .getUpdateTargets,
|
|
18
|
+
dispatchUpdates : require('./functions/orchestrator/helpers/update_helpers') .dispatchUpdates };
|
|
19
|
+
// Dispatcher
|
|
20
|
+
const dispatcher = { handleRequest : require('./functions/dispatcher/index') .handleRequest ,
|
|
21
|
+
dispatchTasksInBatches : require('./functions/dispatcher/helpers/dispatch_helpers') .dispatchTasksInBatches };
|
|
22
|
+
// Task Engine
|
|
23
|
+
const taskEngine = { handleRequest : require('./functions/task-engine/handler_creator') .handleRequest ,
|
|
24
|
+
handleDiscover : require('./functions/task-engine/helpers/discover_helpers') .handleDiscover,
|
|
25
|
+
handleVerify : require('./functions/task-engine/helpers/verify_helpers') .handleVerify ,
|
|
26
|
+
handleUpdate : require('./functions/task-engine/helpers/update_helpers') .handleUpdate };
|
|
27
|
+
// Computation System
|
|
28
|
+
const computationSystem = { runComputationPass : require('./functions/computation-system/helpers/computation_pass_runner') .runComputationPass,
|
|
29
|
+
dataLoader : require('./functions/computation-system/utils/data_loader'),
|
|
30
|
+
computationUtils : require('./functions/computation-system/utils/utils') };
|
|
31
|
+
// API
|
|
32
|
+
const api = { createApiApp : require('./functions/generic-api/index') .createApiApp,
|
|
33
|
+
helpers : require('./functions/generic-api/helpers/api_helpers') };
|
|
34
|
+
// Maintenance
|
|
35
|
+
const maintenance = { runSpeculatorCleanup : require('./functions/speculator-cleanup-orchestrator/helpers/cleanup_helpers') .runCleanup,
|
|
36
|
+
handleInvalidSpeculator : require('./functions/invalid-speculator-handler/helpers/handler_helpers') .handleInvalidSpeculator,
|
|
37
|
+
runFetchInsights : require('./functions/fetch-insights/helpers/handler_helpers') .fetchAndStoreInsights,
|
|
38
|
+
runFetchPrices : require('./functions/etoro-price-fetcher/helpers/handler_helpers') .fetchAndStorePrices,
|
|
39
|
+
runSocialOrchestrator : require('./functions/social-orchestrator/helpers/orchestrator_helpers') .runSocialOrchestrator,
|
|
40
|
+
handleSocialTask : require('./functions/social-task-handler/helpers/handler_helpers') .handleSocialTask,
|
|
41
|
+
runBackfillAssetPrices : require('./functions/price-backfill/helpers/handler_helpers') .runBackfillAssetPrices };
|
|
42
|
+
// Proxy
|
|
43
|
+
const proxy = { handlePost : require('./functions/appscript-api/index') .handlePost };
|
|
44
|
+
|
|
45
|
+
module.exports = { pipe: { core, orchestrator, dispatcher, taskEngine, computationSystem, api, maintenance, proxy } };
|