bulltrackers-module 1.0.14 → 1.0.16
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/index.js +4 -0
- package/functions/core/utils/intelligent_header_manager.js +185 -0
- package/functions/core/utils/intelligent_proxy_manager.js +238 -0
- package/functions/dispatcher/helpers/dispatch_helpers.js +50 -0
- package/functions/dispatcher/index.js +52 -0
- package/index.js +2 -0
- package/package.json +3 -2
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
const firestoreUtils = require('./firestore_utils');
|
|
6
6
|
const pubsubUtils = require('./pubsub_utils');
|
|
7
7
|
const loggingWrapper = require('./logging_wrapper');
|
|
8
|
+
const { IntelligentHeaderManager } = require('./intelligent_header_manager'); // <-- ADD
|
|
9
|
+
const { IntelligentProxyManager } = require('./intelligent_proxy_manager'); // <-- ADD
|
|
8
10
|
|
|
9
11
|
module.exports = {
|
|
10
12
|
firestore: firestoreUtils,
|
|
11
13
|
pubsub: pubsubUtils,
|
|
12
14
|
logging: loggingWrapper,
|
|
15
|
+
IntelligentHeaderManager, // <-- ADD
|
|
16
|
+
IntelligentProxyManager, // <-- ADD
|
|
13
17
|
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview
|
|
3
|
+
* Manages a pool of different browser headers. It selects headers based on their historical success rate
|
|
4
|
+
* to mimic real user traffic and avoid being blocked. Performance data for each header
|
|
5
|
+
* is stored and retrieved from Firestore.
|
|
6
|
+
* * This module is designed to be reusable and receives all dependencies
|
|
7
|
+
* (firestore, logger) and configuration via its constructor.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { FieldValue } = require('@google-cloud/firestore');
|
|
11
|
+
|
|
12
|
+
class IntelligentHeaderManager {
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} firestore - An initialized Firestore instance.
|
|
15
|
+
* @param {object} logger - A logger instance (e.g., from sharedsetup).
|
|
16
|
+
* @param {object} config - Configuration object.
|
|
17
|
+
* @param {string} config.headersCollectionName - The name of the Firestore collection for headers.
|
|
18
|
+
* @param {number} config.cacheDurationMs - How long to cache headers in memory (in ms).
|
|
19
|
+
* @param {string} config.fallbackUserAgent - A fallback User-Agent if loading fails.
|
|
20
|
+
*/
|
|
21
|
+
constructor(firestore, logger, config) {
|
|
22
|
+
if (!firestore || !logger || !config) {
|
|
23
|
+
throw new Error("IntelligentHeaderManager requires firestore, logger, and config objects.");
|
|
24
|
+
}
|
|
25
|
+
if (!config.headersCollectionName || !config.cacheDurationMs || !config.fallbackUserAgent) {
|
|
26
|
+
throw new Error("IntelligentHeaderManager config is missing required keys (headersCollectionName, cacheDurationMs, fallbackUserAgent).");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.firestore = firestore;
|
|
30
|
+
this.logger = logger;
|
|
31
|
+
|
|
32
|
+
// Load from config
|
|
33
|
+
this.collectionName = config.headersCollectionName;
|
|
34
|
+
this.cacheDuration = config.cacheDurationMs;
|
|
35
|
+
this.fallbackUserAgent = config.fallbackUserAgent;
|
|
36
|
+
|
|
37
|
+
// Internal state
|
|
38
|
+
this.headers = [];
|
|
39
|
+
this.lastFetched = null;
|
|
40
|
+
this.performanceUpdates = {};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fetches and caches header documents from Firestore. If the cache is fresh, it does nothing.
|
|
45
|
+
* @private
|
|
46
|
+
* @returns {Promise<void>}
|
|
47
|
+
*/
|
|
48
|
+
async _loadHeaders() {
|
|
49
|
+
const now = new Date();
|
|
50
|
+
if (this.lastFetched && (now - this.lastFetched < this.cacheDuration) && this.headers.length > 0) {
|
|
51
|
+
return; // Cache is fresh
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
this.logger.log('INFO', '[HeaderManager] Refreshing header performance data from Firestore...');
|
|
56
|
+
const snapshot = await this.firestore.collection(this.collectionName).get();
|
|
57
|
+
|
|
58
|
+
if (snapshot.empty) {
|
|
59
|
+
throw new Error(`No documents found in headers collection: ${this.collectionName}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.headers = snapshot.docs.map(doc => ({
|
|
63
|
+
id: doc.id,
|
|
64
|
+
data: doc.data().header, // Assuming header object is stored in 'header' field
|
|
65
|
+
performance: {
|
|
66
|
+
total: doc.data().totalRequests || 0,
|
|
67
|
+
success: doc.data().successfulRequests || 0
|
|
68
|
+
}
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
if (this.headers.length === 0) {
|
|
72
|
+
throw new Error("Header collection was queried but returned no usable headers.");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.lastFetched = new Date();
|
|
76
|
+
this.logger.log('INFO', `[HeaderManager] Successfully loaded ${this.headers.length} headers.`);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
this.logger.log('ERROR', '[HeaderManager] Failed to load headers from Firestore. Using fallback.', {
|
|
79
|
+
errorMessage: error.message,
|
|
80
|
+
collection: this.collectionName
|
|
81
|
+
});
|
|
82
|
+
// Ensure fallback
|
|
83
|
+
if (this.headers.length === 0) {
|
|
84
|
+
this.headers = [{
|
|
85
|
+
id: 'fallback',
|
|
86
|
+
data: { 'User-Agent': this.fallbackUserAgent },
|
|
87
|
+
performance: { total: 1, success: 1 }
|
|
88
|
+
}];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Selects the best available header based on success rate.
|
|
95
|
+
* Uses a weighted random selection to balance exploration and exploitation.
|
|
96
|
+
* @returns {Promise<{id: string, header: object}>}
|
|
97
|
+
*/
|
|
98
|
+
async selectHeader() {
|
|
99
|
+
await this._loadHeaders();
|
|
100
|
+
|
|
101
|
+
if (!this.headers || this.headers.length === 0) {
|
|
102
|
+
this.logger.log('WARN', '[HeaderManager] No headers available, returning fallback.');
|
|
103
|
+
return { id: 'fallback', header: { 'User-Agent': this.fallbackUserAgent } };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Calculate total score for weighted random selection
|
|
107
|
+
let totalScore = 0;
|
|
108
|
+
const weightedHeaders = this.headers.map(h => {
|
|
109
|
+
const successRate = (h.performance.total === 0) ? 0.5 : (h.performance.success / h.performance.total);
|
|
110
|
+
// Add-1 smoothing to avoid zero probability for new headers
|
|
111
|
+
const score = (successRate + 1) * 10;
|
|
112
|
+
totalScore += score;
|
|
113
|
+
return { ...h, score };
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Select a random value
|
|
117
|
+
let random = Math.random() * totalScore;
|
|
118
|
+
|
|
119
|
+
for (const h of weightedHeaders) {
|
|
120
|
+
if (random < h.score) {
|
|
121
|
+
return { id: h.id, header: h.data };
|
|
122
|
+
}
|
|
123
|
+
random -= h.score;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Fallback in case of rounding errors
|
|
127
|
+
const fallbackHeader = this.headers[0];
|
|
128
|
+
return { id: fallbackHeader.id, header: fallbackHeader.data };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Records the performance of a header request in memory.
|
|
133
|
+
* @param {string} headerId - The ID of the header used.
|
|
134
|
+
* @param {boolean} success - Whether the request was successful.
|
|
135
|
+
*/
|
|
136
|
+
updatePerformance(headerId, success) {
|
|
137
|
+
if (headerId === 'fallback') return; // Do not track performance of the fallback
|
|
138
|
+
|
|
139
|
+
if (!this.performanceUpdates[headerId]) {
|
|
140
|
+
this.performanceUpdates[headerId] = { successes: 0, failures: 0 };
|
|
141
|
+
}
|
|
142
|
+
if (success) {
|
|
143
|
+
this.performanceUpdates[headerId].successes++;
|
|
144
|
+
} else {
|
|
145
|
+
this.performanceUpdates[headerId].failures++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Flushes the aggregated performance updates to Firestore.
|
|
151
|
+
* @returns {Promise<void>}
|
|
152
|
+
*/
|
|
153
|
+
async flushPerformanceUpdates() {
|
|
154
|
+
const updatesToFlush = this.performanceUpdates;
|
|
155
|
+
this.performanceUpdates = {}; // Reset the local cache
|
|
156
|
+
|
|
157
|
+
if (Object.keys(updatesToFlush).length === 0) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.logger.log('INFO', `[HeaderManager] Flushing performance updates for ${Object.keys(updatesToFlush).length} headers.`);
|
|
162
|
+
const batch = this.firestore.batch();
|
|
163
|
+
|
|
164
|
+
for (const headerId in updatesToFlush) {
|
|
165
|
+
const updates = updatesToFlush[headerId];
|
|
166
|
+
const docRef = this.firestore.collection(this.collectionName).doc(headerId);
|
|
167
|
+
batch.update(docRef, {
|
|
168
|
+
totalRequests: FieldValue.increment(updates.successes + updates.failures),
|
|
169
|
+
successfulRequests: FieldValue.increment(updates.successes),
|
|
170
|
+
lastUsed: FieldValue.serverTimestamp()
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
await batch.commit();
|
|
176
|
+
this.logger.log('SUCCESS', '[HeaderManager] Successfully flushed header performance updates to Firestore.');
|
|
177
|
+
} catch (error) {
|
|
178
|
+
this.logger.log('ERROR', '[HeaderManager] Failed to commit header performance batch.', { error: error.message });
|
|
179
|
+
// Put updates back in memory to try again next time
|
|
180
|
+
this.performanceUpdates = updatesToFlush;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { IntelligentHeaderManager };
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Manages a pool of proxies (AppScript URLs) using a simple locking mechanism.
|
|
3
|
+
* It selects an available (unlocked) proxy for each request and locks it upon failure.
|
|
4
|
+
* * This module is designed to be reusable and receives all dependencies
|
|
5
|
+
* (firestore, logger) and configuration via its constructor.
|
|
6
|
+
*/
|
|
7
|
+
const { FieldValue } = require('@google-cloud/firestore');
|
|
8
|
+
|
|
9
|
+
class IntelligentProxyManager {
|
|
10
|
+
/**
|
|
11
|
+
* @param {object} firestore - An initialized Firestore instance.
|
|
12
|
+
* @param {object} logger - A logger instance (e.g., from sharedsetup).
|
|
13
|
+
* @param {object} config - Configuration object.
|
|
14
|
+
* @param {string[]} config.proxyUrls - An array of AppScript proxy URLs.
|
|
15
|
+
* @param {number} config.cacheDurationMs - How long to cache proxy status in memory (in ms).
|
|
16
|
+
* @param {string} config.proxiesCollectionName - Firestore collection to check for locks.
|
|
17
|
+
* @param {string} config.proxyPerformanceDocPath - Firestore doc path for performance tracking.
|
|
18
|
+
*/
|
|
19
|
+
constructor(firestore, logger, config) {
|
|
20
|
+
if (!firestore || !logger || !config) {
|
|
21
|
+
throw new Error("IntelligentProxyManager requires firestore, logger, and config objects.");
|
|
22
|
+
}
|
|
23
|
+
if (!config.proxyUrls || !config.cacheDurationMs || !config.proxiesCollectionName || !config.proxyPerformanceDocPath) {
|
|
24
|
+
throw new Error("IntelligentProxyManager config is missing required keys (proxyUrls, cacheDurationMs, proxiesCollectionName, proxyPerformanceDocPath).");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.firestore = firestore;
|
|
28
|
+
this.logger = logger;
|
|
29
|
+
|
|
30
|
+
// Load from config
|
|
31
|
+
this.proxyUrls = config.proxyUrls.filter(Boolean); // Filter out any empty/null URLs
|
|
32
|
+
this.CONFIG_CACHE_DURATION_MS = config.cacheDurationMs;
|
|
33
|
+
this.PROXIES_COLLECTION = config.proxiesCollectionName;
|
|
34
|
+
this.PERFORMANCE_DOC_PATH = config.proxyPerformanceDocPath;
|
|
35
|
+
|
|
36
|
+
// Internal state
|
|
37
|
+
this.proxies = {}; // Stores { owner, url, status ('unlocked', 'locked') }
|
|
38
|
+
this.configLastLoaded = 0;
|
|
39
|
+
|
|
40
|
+
if (this.proxyUrls.length === 0) {
|
|
41
|
+
this.logger.log('WARN', '[ProxyManager] No proxy URLs provided in config.');
|
|
42
|
+
} else {
|
|
43
|
+
this.logger.log('INFO', `[ProxyManager] Initialized with ${this.proxyUrls.length} proxies and Locking Mechanism.`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Loads proxy configuration and lock status from Firestore.
|
|
49
|
+
* Caches the configuration to reduce frequent database reads.
|
|
50
|
+
*/
|
|
51
|
+
async _loadConfig() {
|
|
52
|
+
if (Date.now() - this.configLastLoaded < this.CONFIG_CACHE_DURATION_MS) {
|
|
53
|
+
return; // Cache is fresh
|
|
54
|
+
}
|
|
55
|
+
if (this.proxyUrls.length === 0) {
|
|
56
|
+
return; // No proxies to load
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.logger.log('INFO', "[ProxyManager] Refreshing proxy configuration and lock status...");
|
|
60
|
+
try {
|
|
61
|
+
const tempProxyStatus = {};
|
|
62
|
+
|
|
63
|
+
// 1. Initialize all known proxies from config
|
|
64
|
+
for (const url of this.proxyUrls) {
|
|
65
|
+
const owner = new URL(url).hostname; // Or derive a unique ID differently
|
|
66
|
+
tempProxyStatus[owner] = { owner, url, status: 'unlocked' }; // Default to unlocked
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 2. Load performance doc to get lock statuses
|
|
70
|
+
const doc = await this.firestore.doc(this.PERFORMANCE_DOC_PATH).get();
|
|
71
|
+
if (doc.exists) {
|
|
72
|
+
const data = doc.data();
|
|
73
|
+
if (data.locks) {
|
|
74
|
+
for (const owner in data.locks) {
|
|
75
|
+
if (tempProxyStatus[owner] && data.locks[owner].locked === true) {
|
|
76
|
+
tempProxyStatus[owner].status = 'locked';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.proxies = tempProxyStatus;
|
|
83
|
+
this.configLastLoaded = Date.now();
|
|
84
|
+
this.logger.log('SUCCESS', `[ProxyManager] Refreshed ${Object.keys(this.proxies).length} proxy statuses.`);
|
|
85
|
+
|
|
86
|
+
} catch (error) {
|
|
87
|
+
this.logger.log('ERROR', '[ProxyManager] Failed to load proxy config from Firestore.', {
|
|
88
|
+
errorMessage: error.message,
|
|
89
|
+
path: this.PERFORMANCE_DOC_PATH
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Selects an available (unlocked) proxy from the in-memory cache.
|
|
96
|
+
* @returns {Promise<{owner: string, url: string}>}
|
|
97
|
+
*/
|
|
98
|
+
async _selectProxy() {
|
|
99
|
+
await this._loadConfig();
|
|
100
|
+
|
|
101
|
+
const unlockedProxies = Object.values(this.proxies).filter(p => p.status === 'unlocked');
|
|
102
|
+
|
|
103
|
+
if (unlockedProxies.length === 0) {
|
|
104
|
+
this.logger.log('ERROR', '[ProxyManager] All proxies are locked. No proxy available.');
|
|
105
|
+
throw new Error("All proxies are locked. Cannot make request.");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Return a random unlocked proxy
|
|
109
|
+
const selected = unlockedProxies[Math.floor(Math.random() * unlockedProxies.length)];
|
|
110
|
+
return { owner: selected.owner, url: selected.url };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Locks a proxy by setting its status in memory and writing to Firestore.
|
|
115
|
+
* @param {string} owner - The owner/ID of the proxy to lock.
|
|
116
|
+
*/
|
|
117
|
+
async lockProxy(owner) {
|
|
118
|
+
// 1. Update in-memory cache immediately
|
|
119
|
+
if (this.proxies[owner]) {
|
|
120
|
+
this.proxies[owner].status = 'locked';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.logger.log('WARN', `[ProxyManager] Locking proxy: ${owner}`);
|
|
124
|
+
|
|
125
|
+
// 2. Update Firestore
|
|
126
|
+
try {
|
|
127
|
+
const docRef = this.firestore.doc(this.PERFORMANCE_DOC_PATH);
|
|
128
|
+
// Use dot notation to update a specific field in the 'locks' map
|
|
129
|
+
await docRef.set({
|
|
130
|
+
locks: {
|
|
131
|
+
[owner]: {
|
|
132
|
+
locked: true,
|
|
133
|
+
lastLocked: FieldValue.serverTimestamp()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}, { merge: true });
|
|
137
|
+
} catch (error) {
|
|
138
|
+
this.logger.log('ERROR', `[ProxyManager] Failed to write lock for ${owner} to Firestore.`, { errorMessage: error.message });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Makes a fetch request using a selected proxy.
|
|
144
|
+
* @param {string} targetUrl - The URL to fetch.
|
|
145
|
+
* @param {object} options - Fetch options (e.g., headers).
|
|
146
|
+
* @returns {Promise<object>} A mock Response object.
|
|
147
|
+
*/
|
|
148
|
+
async fetch(targetUrl, options = {}) {
|
|
149
|
+
let proxy = null;
|
|
150
|
+
try {
|
|
151
|
+
// 1. Select Proxy
|
|
152
|
+
proxy = await this._selectProxy();
|
|
153
|
+
} catch (error) {
|
|
154
|
+
// No proxies available
|
|
155
|
+
return { ok: false, status: 503, error: { message: error.message }, headers: new Headers() };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 2. Make Request
|
|
159
|
+
const response = await this._fetchViaAppsScript(proxy.url, targetUrl, options);
|
|
160
|
+
|
|
161
|
+
// 3. Handle Proxy Failure (e.g., quota error, network error)
|
|
162
|
+
if (!response.ok && response.isUrlFetchError) {
|
|
163
|
+
// isUrlFetchError is a custom flag from _fetchViaAppsScript
|
|
164
|
+
await this.lockProxy(proxy.owner);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return response;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Internal function to call the Google AppScript proxy.
|
|
172
|
+
* @private
|
|
173
|
+
*/
|
|
174
|
+
async _fetchViaAppsScript(proxyUrl, targetUrl, options) {
|
|
175
|
+
const payload = {
|
|
176
|
+
url: targetUrl,
|
|
177
|
+
options: options
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const response = await fetch(proxyUrl, {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
headers: { 'Content-Type': 'application/json' },
|
|
184
|
+
body: JSON.stringify(payload)
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
const errorText = await response.text();
|
|
189
|
+
this.logger.log('WARN', `[ProxyManager] Proxy infrastructure itself failed.`, {
|
|
190
|
+
status: response.status,
|
|
191
|
+
proxy: proxyUrl,
|
|
192
|
+
error: errorText
|
|
193
|
+
});
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
status: response.status,
|
|
197
|
+
isUrlFetchError: true, // Flag this as a proxy infrastructure error
|
|
198
|
+
error: { message: `Proxy infrastructure failed with status ${response.status}` },
|
|
199
|
+
headers: response.headers,
|
|
200
|
+
text: () => Promise.resolve(errorText)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const proxyResponse = await response.json();
|
|
205
|
+
|
|
206
|
+
if (proxyResponse.error) {
|
|
207
|
+
const errorMsg = proxyResponse.error.message || '';
|
|
208
|
+
// Check for Google-side quota errors
|
|
209
|
+
if (errorMsg.toLowerCase().includes('service invoked too many times')) {
|
|
210
|
+
this.logger.log('WARN', `[ProxyManager] Proxy quota error: ${proxyUrl}`, { error: proxyResponse.error });
|
|
211
|
+
return { ok: false, status: 500, error: proxyResponse.error, isUrlFetchError: true, headers: new Headers() };
|
|
212
|
+
}
|
|
213
|
+
// Other errors returned by the AppScript
|
|
214
|
+
return { ok: false, status: 500, error: proxyResponse.error, headers: new Headers(), text: () => Promise.resolve(errorMsg) };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Success
|
|
218
|
+
return {
|
|
219
|
+
ok: proxyResponse.statusCode >= 200 && proxyResponse.statusCode < 300,
|
|
220
|
+
status: proxyResponse.statusCode,
|
|
221
|
+
headers: new Headers(proxyResponse.headers || {}),
|
|
222
|
+
json: () => Promise.resolve(JSON.parse(proxyResponse.body)),
|
|
223
|
+
text: () => Promise.resolve(proxyResponse.body),
|
|
224
|
+
};
|
|
225
|
+
} catch (networkError) {
|
|
226
|
+
this.logger.log('ERROR', `[ProxyManager] Network error calling proxy: ${proxyUrl}`, { errorMessage: networkError.message });
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
status: 0, // Network errors don't have a status
|
|
230
|
+
isUrlFetchError: true, // Flag this as a proxy infrastructure error
|
|
231
|
+
error: { message: `Network error: ${networkError.message}` },
|
|
232
|
+
headers: new Headers()
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = { IntelligentProxyManager };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Contains the core logic for dispatching tasks in batches.
|
|
3
|
+
*/
|
|
4
|
+
const { logger } = require("sharedsetup")(__filename); // Assuming sharedsetup is available
|
|
5
|
+
|
|
6
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Publishes tasks in batches with delays.
|
|
10
|
+
* @param {Array} tasks - Array of tasks to publish.
|
|
11
|
+
* @param {object} pubsubClient - Initialized Google Cloud PubSub client.
|
|
12
|
+
* @param {object} config - Configuration object.
|
|
13
|
+
* @param {string} config.topicName - Target Pub/Sub topic name.
|
|
14
|
+
* @param {number} config.batchSize - Number of tasks per batch.
|
|
15
|
+
* @param {number} config.batchDelayMs - Delay between batches in milliseconds.
|
|
16
|
+
*/
|
|
17
|
+
async function dispatchTasksInBatches(tasks, pubsubClient, config) {
|
|
18
|
+
const { topicName, batchSize, batchDelayMs } = config;
|
|
19
|
+
const topic = pubsubClient.topic(topicName);
|
|
20
|
+
let totalTasksQueued = 0;
|
|
21
|
+
|
|
22
|
+
logger.log('INFO', `[Module Dispatcher] Received ${tasks.length} tasks. Creating batches...`);
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < tasks.length; i += batchSize) {
|
|
25
|
+
const batch = tasks.slice(i, i + batchSize);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Publish messages for the current batch
|
|
29
|
+
await Promise.all(batch.map(task => topic.publishMessage({ json: task })));
|
|
30
|
+
totalTasksQueued += batch.length;
|
|
31
|
+
logger.log('INFO', `[Module Dispatcher] Dispatched batch ${Math.ceil((i + 1) / batchSize)} with ${batch.length} tasks.`);
|
|
32
|
+
|
|
33
|
+
// Apply delay if it's not the last batch
|
|
34
|
+
if (i + batchSize < tasks.length) {
|
|
35
|
+
await sleep(batchDelayMs);
|
|
36
|
+
}
|
|
37
|
+
} catch (publishError) {
|
|
38
|
+
logger.log('ERROR', `[Module Dispatcher] Failed to publish batch ${Math.ceil((i + 1) / batchSize)}. Error: ${publishError.message}`, { errorStack: publishError.stack });
|
|
39
|
+
// Decide on error handling: continue to next batch or throw?
|
|
40
|
+
// For now, log and continue, but this might need adjustment based on requirements.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
logger.log('SUCCESS', `[Module Dispatcher] Successfully dispatched ${totalTasksQueued} tasks in ${Math.ceil(tasks.length / batchSize)} batches.`);
|
|
45
|
+
return totalTasksQueued; // Return count for confirmation
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
dispatchTasksInBatches
|
|
50
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main entry point for the Dispatcher function logic within the module.
|
|
3
|
+
*/
|
|
4
|
+
const { logger } = require("sharedsetup")(__filename); // Assuming sharedsetup is available
|
|
5
|
+
const { dispatchTasksInBatches } = require('./helpers/dispatch_helpers');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates the Pub/Sub triggered Cloud Function handler for the Dispatcher.
|
|
9
|
+
* @param {object} pubsubClient - Initialized Google Cloud PubSub client.
|
|
10
|
+
* @param {object} config - Configuration object loaded from the calling function's context.
|
|
11
|
+
* @returns {Function} The Cloud Function handler.
|
|
12
|
+
*/
|
|
13
|
+
function createDispatcherHandler(pubsubClient, config) {
|
|
14
|
+
return async (message, context) => {
|
|
15
|
+
try {
|
|
16
|
+
// 1. Decode Message
|
|
17
|
+
if (!message.data) {
|
|
18
|
+
logger.log('WARN', '[Module Dispatcher] Received message without data.');
|
|
19
|
+
return; // Acknowledge and exit gracefully
|
|
20
|
+
}
|
|
21
|
+
const decodedMessage = JSON.parse(Buffer.from(message.data, 'base64').toString());
|
|
22
|
+
const { tasks } = decodedMessage;
|
|
23
|
+
|
|
24
|
+
if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
|
|
25
|
+
logger.log('WARN', '[Module Dispatcher] Received message with no valid tasks. Nothing to do.');
|
|
26
|
+
return; // Acknowledge and exit
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 2. Validate Config (Basic check)
|
|
30
|
+
if (!config || !config.topicName || !config.batchSize || !config.batchDelayMs) {
|
|
31
|
+
logger.log('ERROR', '[Module Dispatcher] Invalid configuration provided.', { config });
|
|
32
|
+
throw new Error("Dispatcher module received invalid configuration.");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 3. Dispatch Tasks
|
|
36
|
+
await dispatchTasksInBatches(tasks, pubsubClient, config);
|
|
37
|
+
|
|
38
|
+
// If we reach here, the process (including logging success/errors) is handled within dispatchTasksInBatches
|
|
39
|
+
|
|
40
|
+
} catch (error) {
|
|
41
|
+
// Catch errors during decoding or major setup issues
|
|
42
|
+
logger.log('ERROR', '[Module Dispatcher] FATAL error processing message', { errorMessage: error.message, errorStack: error.stack });
|
|
43
|
+
// Re-throw to signal Pub/Sub to potentially retry (depending on subscription settings)
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
createDispatcherHandler,
|
|
51
|
+
helpers: { dispatchTasksInBatches } // Export helpers if needed elsewhere
|
|
52
|
+
};
|
package/index.js
CHANGED
|
@@ -9,6 +9,7 @@ const Orchestrator = require('./functions/orchestrator');
|
|
|
9
9
|
const TaskEngine = require('./functions/task-engine');
|
|
10
10
|
const ComputationSystem = require('./functions/computation-system');
|
|
11
11
|
const GenericAPI = require('./functions/generic-api'); // <-- ADD THIS
|
|
12
|
+
const Dispatcher = require('./functions/dispatcher'); // <-- ADD THIS
|
|
12
13
|
|
|
13
14
|
module.exports = {
|
|
14
15
|
core,
|
|
@@ -16,4 +17,5 @@ module.exports = {
|
|
|
16
17
|
TaskEngine,
|
|
17
18
|
ComputationSystem,
|
|
18
19
|
GenericAPI, // <-- AND ADD THIS
|
|
20
|
+
Dispatcher, // <-- AND ADD THI
|
|
19
21
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bulltrackers-module",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "Helper Functions for Bulltrackers.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"functions/task-engine/",
|
|
10
10
|
"functions/core/",
|
|
11
11
|
"functions/computation-system/",
|
|
12
|
-
"functions/generic-api/"
|
|
12
|
+
"functions/generic-api/",
|
|
13
|
+
"functions/dispatcher/"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
16
|
"test": "echo \"Error: no test specified\" && exit 1"
|