File size: 10,791 Bytes
212a3a7 |
1 2 3 4 5 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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
import { EventEmitter } from 'events';
import { randomUUID } from 'crypto';
export default class BrowserPool extends EventEmitter {
constructor(options = {}) {
super();
this.chromium = options.chromium;
if (!this.chromium) throw new Error('Chromium instance must be provided');
this.maxSize = options.maxSize || 5;
this.minSize = options.minSize || 1;
this.maxUsage = options.maxUsage || 100;
this.maxIdleTime = options.maxIdleTime || 300000;
this.createTimeout = options.createTimeout || 30000;
this.validateInterval = options.validateInterval || 60000;
this.getTimeout = options.getTimeout || 30000;
this.availableInstances = [];
this.busyInstances = new Set();
this.totalInstances = 0;
this.waitingQueue = [];
this.stats = { created: 0, destroyed: 0, borrowed: 0, returned: 0, timeouts: 0, errors: 0 };
this.isShuttingDown = false;
this.healthCheckInterval = null;
}
async initialize() {
console.log(`[BrowserPool] Initializing pool with min: ${this.minSize}, max: ${this.maxSize}`);
const createPromises = Array.from({ length: this.minSize }, () => this.createInstance());
try {
await Promise.all(createPromises);
console.log(`[BrowserPool] Pool initialized with ${this.availableInstances.length} instances.`);
} catch (error) {
console.error('[BrowserPool] Failed to initialize pool:', error.message);
await this.shutdown();
throw error;
}
this.startHealthCheck();
}
async createInstance() {
if (this.isShuttingDown) { throw new Error('Pool is shutting down'); }
if (this.totalInstances >= this.maxSize) { return null; }
const startTime = Date.now();
let browser = null;
this.totalInstances++;
try {
console.log('[BrowserPool] Creating new browser instance...');
browser = await Promise.race([
this.chromium.launch({
headless: true,
args: [
// '--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled',
// '--disable-web-security', '--autoplay-policy=no-user-gesture-required',
// '--disable-dev-shm-usage', '--disable-gpu', '--single-process', '--no-zygote',
// '--disable-infobars', '--disable-notifications', '--disable-extensions',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-blink-features=AutomationControlled',
'--disable-web-security',
'--autoplay-policy=no-user-gesture-required',
'--disable-dev-shm-usage', // 避免共享内存问题
'--disable-gpu', // 减少资源使用
'--no-first-run', // 跳过首次运行设置
'--disable-default-apps' // 禁用默认应用
]
}),
new Promise((_, reject) => setTimeout(() => reject(new Error('Browser creation timeout')), this.createTimeout))
]);
const instance = {
browser, id: `browser_${randomUUID().slice(0, 8)}`, createdAt: Date.now(),
lastUsed: Date.now(), usageCount: 0, isHealthy: true
};
browser.on('disconnected', () => {
console.log(`[BrowserPool] Browser ${instance.id} disconnected unexpectedly.`);
this.destroyInstance(instance, true);
});
this.stats.created++;
console.log(`[BrowserPool] Browser instance created: ${instance.id} (${Date.now() - startTime}ms)`);
this.emit('instanceCreated', instance);
this.availableInstances.push(instance);
this.processWaitingQueue();
return instance;
} catch (error) {
this.totalInstances--;
this.stats.errors++;
console.error('[BrowserPool] Failed to create browser instance:', error.message);
if (browser) await browser.close().catch(e => console.error(`Error closing failed browser: ${e.message}`));
throw error;
}
}
get() {
if (this.isShuttingDown) return Promise.reject(new Error('Pool is shutting down'));
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.waitingQueue = this.waitingQueue.filter(w => w.resolve !== resolve);
this.stats.timeouts++;
reject(new Error(`Browser acquisition timeout after ${this.getTimeout}ms`));
}, this.getTimeout);
const tryAcquire = async () => {
while (this.availableInstances.length > 0) {
const instance = this.availableInstances.shift();
if (await this.validateInstance(instance)) {
this.busyInstances.add(instance);
instance.lastUsed = Date.now();
instance.usageCount++;
this.stats.borrowed++;
clearTimeout(timeoutId);
return resolve(instance);
} else {
await this.destroyInstance(instance);
}
}
if (this.totalInstances < this.maxSize) {
this.createInstance().catch(err => console.error(`[BrowserPool] Background instance creation failed: ${err.message}`));
}
this.waitingQueue.push({ resolve, reject, timeoutId });
console.log(`[BrowserPool] No available instances, request added to waiting queue (size: ${this.waitingQueue.length})`);
};
tryAcquire();
});
}
async release(instance) {
if (!instance || !this.busyInstances.has(instance)) { return; }
this.busyInstances.delete(instance);
this.stats.returned++;
if (this.isShuttingDown || !await this.validateInstance(instance) || instance.usageCount >= this.maxUsage) {
await this.destroyInstance(instance);
} else {
try {
const contexts = instance.browser.contexts();
// 默认上下文是第一个,我们不关闭它,只关闭后续创建的
for (let i = 1; i < contexts.length; i++) {
await contexts[i].close();
}
this.availableInstances.push(instance);
} catch (e) {
console.warn(`[BrowserPool] Error cleaning up contexts for ${instance.id}, destroying it.`, e.message);
await this.destroyInstance(instance);
}
}
this.processWaitingQueue();
}
async destroyInstance(instance, isDisconnected = false) {
if (!instance) return;
this.busyInstances.delete(instance);
const index = this.availableInstances.indexOf(instance);
if (index > -1) this.availableInstances.splice(index, 1);
if (!isDisconnected && instance.browser && instance.browser.isConnected()) {
await instance.browser.close().catch(e => console.error(`[BrowserPool] Error closing browser on destroy: ${e.message}`));
}
if (this.totalInstances > 0) this.totalInstances--;
this.stats.destroyed++;
console.log(`[BrowserPool] Instance ${instance.id} destroyed.`);
this.emit('instanceDestroyed', instance);
if (!this.isShuttingDown && this.totalInstances < this.minSize) {
this.createInstance().catch(err => console.error(`[BrowserPool] Failed to replenish instance: ${err.message}`));
}
}
processWaitingQueue() {
if (this.waitingQueue.length > 0 && this.availableInstances.length > 0) {
console.log('[BrowserPool] Processing waiting queue...');
const waiter = this.waitingQueue.shift();
const instance = this.availableInstances.shift();
this.busyInstances.add(instance);
instance.lastUsed = Date.now();
instance.usageCount++;
this.stats.borrowed++;
clearTimeout(waiter.timeoutId);
waiter.resolve(instance);
}
}
async validateInstance(instance) {
if (!instance || !instance.browser || !instance.browser.isConnected()) return false;
try {
// 一个更轻量级的检查,创建一个新的 incognito context
const context = await instance.browser.newContext();
await context.close();
return true;
} catch (error) {
console.warn(`[BrowserPool] Instance ${instance.id} validation failed.`);
return false;
}
}
startHealthCheck() {
this.healthCheckInterval = setInterval(() => this.performHealthCheck(), this.validateInterval);
}
async performHealthCheck() {
if (this.isShuttingDown) return;
const now = Date.now();
const idleInstances = this.availableInstances.slice(); // 创建副本以安全遍历
for (const instance of idleInstances) {
if (this.totalInstances > this.minSize && now - instance.lastUsed > this.maxIdleTime) {
console.log(`[BrowserPool] Removing idle instance ${instance.id}`);
await this.destroyInstance(instance);
}
}
}
getStats() {
return {
...this.stats,
pool: {
available: this.availableInstances.length,
busy: this.busyInstances.size,
total: this.totalInstances,
waiting: this.waitingQueue.length
}
};
}
async shutdown() {
console.log('[BrowserPool] Shutting down...');
this.isShuttingDown = true;
if (this.healthCheckInterval) clearInterval(this.healthCheckInterval);
for (const waiter of this.waitingQueue) {
clearTimeout(waiter.timeoutId);
waiter.reject(new Error('Pool is shutting down'));
}
this.waitingQueue = [];
const allInstances = [...this.availableInstances, ...Array.from(this.busyInstances)];
await Promise.all(allInstances.map(instance => instance.browser.close()));
console.log('[BrowserPool] All browser instances closed.');
}
}
// 使用 module.exports 导出类
// module.exports = { BrowserPool };
|