vid / pool /BrowserPool.js
stnh70's picture
Rename BrowserPool.js to pool/BrowserPool.js
db42e4f verified
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 };