A2C工单修复
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
/**
|
||||
* Browser 温池:按 profile:sessionKey 复用 Browser/Page,降低冷启动与 CF 触发
|
||||
* Browser 温池:按 profile:sessionKey 复用 Browser,降低冷启动与 CF 触发
|
||||
*
|
||||
* 同一 key 的 acquire 在 inUse 时排队等待,禁止销毁正在使用的 Browser(修复 A2C 并发误杀)。
|
||||
*/
|
||||
const { POOL_IDLE_MS, MAX_POOLED_BROWSERS, POOL_ENABLED } = require('./constants');
|
||||
const { POOL_IDLE_MS, MAX_POOLED_BROWSERS, POOL_ENABLED, QUEUE_TIMEOUT_MS } = require('./constants');
|
||||
|
||||
class BrowserPool {
|
||||
constructor() {
|
||||
/** @type {Map<string, { browser: any, page: any|null, profile: string, sessionKey: string, lastUsedAt: number, inUse: boolean }>} */
|
||||
this.entries = new Map();
|
||||
/** @type {Map<string, Array<{ resolve: () => void, reject: (err: Error) => void, timer: NodeJS.Timeout|null }>>} */
|
||||
this._waitQueues = new Map();
|
||||
this.hits = 0;
|
||||
this.misses = 0;
|
||||
this.evictions = 0;
|
||||
@@ -14,17 +18,10 @@ class BrowserPool {
|
||||
this._sweepTimer.unref?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} sessionKey
|
||||
* @param {'standard'|'real'} profile
|
||||
*/
|
||||
buildKey(sessionKey, profile) {
|
||||
return `${profile}:${sessionKey}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} browser
|
||||
*/
|
||||
async isBrowserAlive(browser) {
|
||||
if (!browser) return false;
|
||||
try {
|
||||
@@ -38,12 +35,13 @@ class BrowserPool {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
async destroyEntry(key) {
|
||||
const entry = this.entries.get(key);
|
||||
if (!entry) return;
|
||||
if (entry.inUse) {
|
||||
console.warn(`[BrowserPool] 跳过销毁 inUse 条目 key=${key}`);
|
||||
return;
|
||||
}
|
||||
this.entries.delete(key);
|
||||
try {
|
||||
await entry.browser.close();
|
||||
@@ -63,11 +61,43 @@ class BrowserPool {
|
||||
this.destroyEntry(oldestKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} sessionKey
|
||||
* @param {'standard'|'real'} profile
|
||||
* @param {() => Promise<{ browser: any, page: any|null, cleanup: (destroy?: boolean) => Promise<void> }>} launcher
|
||||
*/
|
||||
waitForRelease(key, timeoutMs = QUEUE_TIMEOUT_MS) {
|
||||
const entry = this.entries.get(key);
|
||||
if (!entry || !entry.inUse) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const waiter = {
|
||||
resolve: () => {
|
||||
if (waiter.timer) clearTimeout(waiter.timer);
|
||||
resolve();
|
||||
},
|
||||
reject,
|
||||
timer: timeoutMs > 0
|
||||
? setTimeout(() => {
|
||||
const queue = this._waitQueues.get(key) || [];
|
||||
const idx = queue.indexOf(waiter);
|
||||
if (idx >= 0) queue.splice(idx, 1);
|
||||
reject(new Error('POOL_ACQUIRE_TIMEOUT'));
|
||||
}, timeoutMs)
|
||||
: null,
|
||||
};
|
||||
if (!this._waitQueues.has(key)) {
|
||||
this._waitQueues.set(key, []);
|
||||
}
|
||||
this._waitQueues.get(key).push(waiter);
|
||||
console.log(`[BrowserPool] key=${key} 正在使用,排队等待 (队列 ${this._waitQueues.get(key).length})`);
|
||||
});
|
||||
}
|
||||
|
||||
_wakeNextWaiter(key) {
|
||||
const queue = this._waitQueues.get(key);
|
||||
if (!queue || queue.length === 0) return;
|
||||
const next = queue.shift();
|
||||
if (next?.timer) clearTimeout(next.timer);
|
||||
next?.resolve();
|
||||
}
|
||||
|
||||
async acquire(sessionKey, profile, launcher) {
|
||||
if (!POOL_ENABLED || !sessionKey) {
|
||||
const launched = await launcher();
|
||||
@@ -77,29 +107,38 @@ class BrowserPool {
|
||||
profile,
|
||||
pooled: false,
|
||||
reused: false,
|
||||
release: async (destroy = false) => {
|
||||
await launched.cleanup(destroy);
|
||||
release: async () => {
|
||||
await launched.cleanup(true);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const key = this.buildKey(sessionKey, profile);
|
||||
const existing = this.entries.get(key);
|
||||
if (existing && !existing.inUse && await this.isBrowserAlive(existing.browser)) {
|
||||
existing.inUse = true;
|
||||
existing.lastUsedAt = Date.now();
|
||||
this.hits++;
|
||||
return {
|
||||
browser: existing.browser,
|
||||
page: existing.page,
|
||||
profile,
|
||||
pooled: true,
|
||||
reused: true,
|
||||
release: async (destroy = false) => this.release(key, destroy),
|
||||
};
|
||||
}
|
||||
if (existing) {
|
||||
|
||||
while (true) {
|
||||
const existing = this.entries.get(key);
|
||||
if (!existing) {
|
||||
break;
|
||||
}
|
||||
if (existing.inUse) {
|
||||
await this.waitForRelease(key);
|
||||
continue;
|
||||
}
|
||||
if (await this.isBrowserAlive(existing.browser)) {
|
||||
existing.inUse = true;
|
||||
existing.lastUsedAt = Date.now();
|
||||
this.hits++;
|
||||
return {
|
||||
browser: existing.browser,
|
||||
page: null,
|
||||
profile,
|
||||
pooled: true,
|
||||
reused: true,
|
||||
release: async (destroy = false) => this.release(key, destroy),
|
||||
};
|
||||
}
|
||||
await this.destroyEntry(key);
|
||||
break;
|
||||
}
|
||||
|
||||
this.enforceCapacity(key);
|
||||
@@ -107,7 +146,7 @@ class BrowserPool {
|
||||
const launched = await launcher();
|
||||
this.entries.set(key, {
|
||||
browser: launched.browser,
|
||||
page: launched.page,
|
||||
page: null,
|
||||
profile,
|
||||
sessionKey,
|
||||
lastUsedAt: Date.now(),
|
||||
@@ -124,18 +163,19 @@ class BrowserPool {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {boolean} destroy
|
||||
*/
|
||||
async release(key, destroy = false) {
|
||||
const entry = this.entries.get(key);
|
||||
if (!entry) return;
|
||||
if (!entry) {
|
||||
this._wakeNextWaiter(key);
|
||||
return;
|
||||
}
|
||||
entry.inUse = false;
|
||||
entry.lastUsedAt = Date.now();
|
||||
entry.page = null;
|
||||
if (destroy || !(await this.isBrowserAlive(entry.browser))) {
|
||||
await this.destroyEntry(key);
|
||||
}
|
||||
this._wakeNextWaiter(key);
|
||||
}
|
||||
|
||||
evictIdle() {
|
||||
@@ -151,17 +191,24 @@ class BrowserPool {
|
||||
clearInterval(this._sweepTimer);
|
||||
const keys = [...this.entries.keys()];
|
||||
for (const key of keys) {
|
||||
await this.destroyEntry(key);
|
||||
const entry = this.entries.get(key);
|
||||
if (entry && !entry.inUse) {
|
||||
await this.destroyEntry(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getStats() {
|
||||
let idle = 0;
|
||||
let active = 0;
|
||||
let waiting = 0;
|
||||
for (const entry of this.entries.values()) {
|
||||
if (entry.inUse) active++;
|
||||
else idle++;
|
||||
}
|
||||
for (const queue of this._waitQueues.values()) {
|
||||
waiting += queue.length;
|
||||
}
|
||||
return {
|
||||
enabled: POOL_ENABLED,
|
||||
max: MAX_POOLED_BROWSERS,
|
||||
@@ -169,6 +216,7 @@ class BrowserPool {
|
||||
size: this.entries.size,
|
||||
idle,
|
||||
active,
|
||||
waiting,
|
||||
hits: this.hits,
|
||||
misses: this.misses,
|
||||
evictions: this.evictions,
|
||||
|
||||
Reference in New Issue
Block a user