bulltrackers-module 1.0.155 → 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 +13 -66
- package/index.js +42 -98
- package/package.json +1 -1
|
@@ -6,8 +6,6 @@
|
|
|
6
6
|
* --- MODIFIED: Now includes exponential backoff and retries specifically for rate-limit errors. ---
|
|
7
7
|
*/
|
|
8
8
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
9
|
-
|
|
10
|
-
// --- NEW: Added sleep utility ---
|
|
11
9
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
12
10
|
|
|
13
11
|
class IntelligentProxyManager {
|
|
@@ -33,11 +31,8 @@ class IntelligentProxyManager {
|
|
|
33
31
|
this.proxyLockingEnabled = config.proxyLockingEnabled !== false;
|
|
34
32
|
this.proxies = {};
|
|
35
33
|
this.configLastLoaded = 0;
|
|
36
|
-
|
|
37
|
-
// --- NEW: Retry configuration ---
|
|
38
34
|
this.MAX_RETRIES = 3;
|
|
39
35
|
this.INITIAL_BACKOFF_MS = 1000;
|
|
40
|
-
|
|
41
36
|
if (this.proxyUrls.length === 0) { this.logger.log('WARN', '[ProxyManager] No proxy URLs provided in config.');
|
|
42
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}.`); }
|
|
43
38
|
}
|
|
@@ -68,10 +63,8 @@ class IntelligentProxyManager {
|
|
|
68
63
|
*/
|
|
69
64
|
async _selectProxy() {
|
|
70
65
|
await this._loadConfig();
|
|
71
|
-
|
|
72
66
|
const availableProxies = this.proxyLockingEnabled ? Object.values(this.proxies).filter(p => p.status === 'unlocked') : Object.values(this.proxies);
|
|
73
|
-
if (availableProxies.length === 0) { const errorMsg = this.proxyLockingEnabled ? "All proxies are locked. No proxy available." : "No proxies are loaded. Cannot make request.";
|
|
74
|
-
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); }
|
|
75
68
|
const selected = availableProxies[Math.floor(Math.random() * availableProxies.length)];
|
|
76
69
|
return { owner: selected.owner, url: selected.url };
|
|
77
70
|
}
|
|
@@ -84,8 +77,7 @@ class IntelligentProxyManager {
|
|
|
84
77
|
if (!this.proxyLockingEnabled) { this.logger.log('TRACE', `[ProxyManager] Locking skipped for ${owner} (locking is disabled).`); return; }
|
|
85
78
|
if (this.proxies[owner]) { this.proxies[owner].status = 'locked'; }
|
|
86
79
|
this.logger.log('WARN', `[ProxyManager] Locking proxy: ${owner}`);
|
|
87
|
-
try { const docRef = this.firestore.doc(this.PERFORMANCE_DOC_PATH);
|
|
88
|
-
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 });
|
|
89
81
|
} catch (error) { this.logger.log('ERROR', `[ProxyManager] Failed to write lock for ${owner} to Firestore.`, { errorMessage: error.message }); }
|
|
90
82
|
}
|
|
91
83
|
|
|
@@ -97,46 +89,20 @@ class IntelligentProxyManager {
|
|
|
97
89
|
*/
|
|
98
90
|
async fetch(targetUrl, options = {}) {
|
|
99
91
|
let proxy = null;
|
|
100
|
-
try {
|
|
101
|
-
proxy = await this._selectProxy();
|
|
102
|
-
} catch (error) {
|
|
103
|
-
// This happens if *all* proxies are locked.
|
|
104
|
-
return { ok: false, status: 503, error: { message: error.message }, headers: new Headers() };
|
|
105
|
-
}
|
|
106
|
-
|
|
92
|
+
try { proxy = await this._selectProxy(); } catch (error) { return { ok: false, status: 503, error: { message: error.message }, headers: new Headers() }; }
|
|
107
93
|
let backoff = this.INITIAL_BACKOFF_MS;
|
|
108
94
|
let lastResponse = null;
|
|
109
|
-
|
|
110
95
|
for (let attempt = 1; attempt <= this.MAX_RETRIES; attempt++) {
|
|
111
96
|
const response = await this._fetchViaAppsScript(proxy.url, targetUrl, options);
|
|
112
|
-
lastResponse = response;
|
|
113
|
-
|
|
97
|
+
lastResponse = response;
|
|
114
98
|
// 1. Success
|
|
115
|
-
if (response.ok) {
|
|
116
|
-
return response;
|
|
117
|
-
}
|
|
118
|
-
|
|
99
|
+
if (response.ok) { return response; }
|
|
119
100
|
// 2. Rate Limit Error (Retryable)
|
|
120
|
-
if (response.isRateLimitError) {
|
|
121
|
-
this.logger.log('WARN', `[ProxyManager] Rate limit hit on proxy ${proxy.owner} (Attempt ${attempt}/${this.MAX_RETRIES}). Backing off for ${backoff}ms...`, { url: targetUrl });
|
|
122
|
-
await sleep(backoff);
|
|
123
|
-
backoff *= 2; // Exponential backoff
|
|
124
|
-
// Continue to the next attempt
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
|
|
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; }
|
|
128
102
|
// 3. Other Fetch Error (Non-Retryable, Lock Proxy)
|
|
129
|
-
if (response.isUrlFetchError) {
|
|
130
|
-
this.logger.log('ERROR', `[ProxyManager] Proxy ${proxy.owner} failed (non-rate-limit). Locking proxy.`, { url: targetUrl, status: response.status });
|
|
131
|
-
await this.lockProxy(proxy.owner);
|
|
132
|
-
return response; // Fail fast and return
|
|
133
|
-
}
|
|
134
|
-
|
|
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; }
|
|
135
104
|
// 4. Standard Error (e.g., 404, 500 from *target* URL, not proxy)
|
|
136
|
-
|
|
137
|
-
return response;
|
|
138
|
-
}
|
|
139
|
-
|
|
105
|
+
return response; }
|
|
140
106
|
// If loop finishes, all retries failed (likely all were rate-limit errors)
|
|
141
107
|
this.logger.log('ERROR', `[ProxyManager] Request failed after ${this.MAX_RETRIES} rate-limit retries.`, { url: targetUrl });
|
|
142
108
|
return lastResponse;
|
|
@@ -152,30 +118,16 @@ class IntelligentProxyManager {
|
|
|
152
118
|
const payload = { url: targetUrl, ...options };
|
|
153
119
|
try {
|
|
154
120
|
const response = await fetch(proxyUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
|
155
|
-
|
|
156
|
-
// This is an error with the *proxy function itself* (e.g., 500, 429)
|
|
157
121
|
if (!response.ok) {
|
|
158
122
|
const errorText = await response.text();
|
|
159
123
|
this.logger.log('WARN', `[ProxyManager] Proxy infrastructure itself failed.`, { status: response.status, proxy: proxyUrl, error: errorText });
|
|
160
124
|
const isRateLimit = response.status === 429;
|
|
161
|
-
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) };
|
|
162
|
-
}
|
|
163
|
-
|
|
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) }; }
|
|
164
126
|
const proxyResponse = await response.json();
|
|
165
|
-
|
|
166
|
-
// This is an error *returned by the proxy* (e.g., UrlFetchApp failed)
|
|
167
127
|
if (proxyResponse.error) {
|
|
168
128
|
const errorMsg = proxyResponse.error.message || '';
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
this.logger.log('WARN', `[ProxyManager] Proxy quota error: ${proxyUrl}`, { error: proxyResponse.error });
|
|
172
|
-
return { ok: false, status: 500, error: proxyResponse.error, isUrlFetchError: true, isRateLimitError: true, headers: new Headers() }; // <-- Set flag
|
|
173
|
-
}
|
|
174
|
-
// Other UrlFetchApp error
|
|
175
|
-
return { ok: false, status: 500, error: proxyResponse.error, isUrlFetchError: true, isRateLimitError: false, headers: new Headers(), text: () => Promise.resolve(errorMsg) };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Success. The proxy fetched the target URL.
|
|
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) }; }
|
|
179
131
|
return {
|
|
180
132
|
ok: proxyResponse.statusCode >= 200 && proxyResponse.statusCode < 300,
|
|
181
133
|
status: proxyResponse.statusCode,
|
|
@@ -183,13 +135,8 @@ class IntelligentProxyManager {
|
|
|
183
135
|
json: () => Promise.resolve(JSON.parse(proxyResponse.body)),
|
|
184
136
|
text: () => Promise.resolve(proxyResponse.body),
|
|
185
137
|
isUrlFetchError: false,
|
|
186
|
-
isRateLimitError: false
|
|
187
|
-
|
|
188
|
-
} catch (networkError) {
|
|
189
|
-
this.logger.log('ERROR', `[ProxyManager] Network error calling proxy: ${proxyUrl}`, { errorMessage: networkError.message });
|
|
190
|
-
return { ok: false, status: 0, isUrlFetchError: true, isRateLimitError: false, error: { message: `Network error: ${networkError.message}` }, headers: new Headers() };
|
|
191
|
-
}
|
|
192
|
-
}
|
|
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() }; } }
|
|
193
140
|
}
|
|
194
141
|
|
|
195
142
|
module.exports = { IntelligentProxyManager };
|
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 } };
|