mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(server): queue run-server clients (#16234)
This commit is contained in:
parent
e5cc78af67
commit
db2972792b
@ -54,7 +54,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
|
|||||||
path = options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}`;
|
path = options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}`;
|
||||||
|
|
||||||
// 2. Start the server
|
// 2. Start the server
|
||||||
const server = new PlaywrightServer('use-pre-launched-browser', { path, maxClients: Infinity, enableSocksProxy: false, preLaunchedBrowser: browser });
|
const server = new PlaywrightServer('use-pre-launched-browser', { path, maxConcurrentConnections: Infinity, maxIncomingConnections: Infinity, enableSocksProxy: false, preLaunchedBrowser: browser });
|
||||||
const wsEndpoint = await server.listen(options.port);
|
const wsEndpoint = await server.listen(options.port);
|
||||||
|
|
||||||
// 3. Return the BrowserServer interface
|
// 3. Return the BrowserServer interface
|
||||||
|
|||||||
@ -47,7 +47,9 @@ export function runDriver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runServer(port: number | undefined, path = '/', maxClients = Infinity, enableSocksProxy = true, reuseBrowser = false) {
|
export async function runServer(port: number | undefined, path = '/', maxClients = Infinity, enableSocksProxy = true, reuseBrowser = false) {
|
||||||
const server = new PlaywrightServer(reuseBrowser ? 'reuse-browser' : 'auto', { path, maxClients, enableSocksProxy });
|
const maxIncomingConnections = maxClients;
|
||||||
|
const maxConcurrentConnections = reuseBrowser ? 1 : maxClients;
|
||||||
|
const server = new PlaywrightServer(reuseBrowser ? 'reuse-browser' : 'auto', { path, maxIncomingConnections, maxConcurrentConnections, enableSocksProxy });
|
||||||
const wsEndpoint = await server.listen(port);
|
const wsEndpoint = await server.listen(port);
|
||||||
process.on('exit', () => server.close().catch(console.error));
|
process.on('exit', () => server.close().catch(console.error));
|
||||||
console.log('Listening on ' + wsEndpoint); // eslint-disable-line no-console
|
console.log('Listening on ' + wsEndpoint); // eslint-disable-line no-console
|
||||||
|
|||||||
@ -23,7 +23,7 @@ function launchGridBrowserWorker(gridURL: string, agentId: string, workerId: str
|
|||||||
const log = debug(`pw:grid:worker:${workerId}`);
|
const log = debug(`pw:grid:worker:${workerId}`);
|
||||||
log('created');
|
log('created');
|
||||||
const ws = new WebSocket(gridURL.replace('http://', 'ws://') + `/registerWorker?agentId=${agentId}&workerId=${workerId}`);
|
const ws = new WebSocket(gridURL.replace('http://', 'ws://') + `/registerWorker?agentId=${agentId}&workerId=${workerId}`);
|
||||||
new PlaywrightConnection('auto', ws, { enableSocksProxy: true, browserAlias, launchOptions: {} }, { playwright: null, browser: null }, log, async () => {
|
new PlaywrightConnection(Promise.resolve(), 'auto', ws, { enableSocksProxy: true, browserAlias, launchOptions: {} }, { playwright: null, browser: null }, log, async () => {
|
||||||
log('exiting process');
|
log('exiting process');
|
||||||
setTimeout(() => process.exit(0), 30000);
|
setTimeout(() => process.exit(0), 30000);
|
||||||
// Meanwhile, try to gracefully close all browsers.
|
// Meanwhile, try to gracefully close all browsers.
|
||||||
|
|||||||
@ -48,7 +48,7 @@ export class PlaywrightConnection {
|
|||||||
private _options: Options;
|
private _options: Options;
|
||||||
private _root: Root;
|
private _root: Root;
|
||||||
|
|
||||||
constructor(mode: Mode, ws: WebSocket, options: Options, preLaunched: PreLaunched, log: (m: string) => void, onClose: () => void) {
|
constructor(lock: Promise<void>, mode: Mode, ws: WebSocket, options: Options, preLaunched: PreLaunched, log: (m: string) => void, onClose: () => void) {
|
||||||
this._ws = ws;
|
this._ws = ws;
|
||||||
this._preLaunched = preLaunched;
|
this._preLaunched = preLaunched;
|
||||||
this._options = options;
|
this._options = options;
|
||||||
@ -60,11 +60,13 @@ export class PlaywrightConnection {
|
|||||||
this._debugLog = log;
|
this._debugLog = log;
|
||||||
|
|
||||||
this._dispatcherConnection = new DispatcherConnection();
|
this._dispatcherConnection = new DispatcherConnection();
|
||||||
this._dispatcherConnection.onmessage = message => {
|
this._dispatcherConnection.onmessage = async message => {
|
||||||
|
await lock;
|
||||||
if (ws.readyState !== ws.CLOSING)
|
if (ws.readyState !== ws.CLOSING)
|
||||||
ws.send(JSON.stringify(message));
|
ws.send(JSON.stringify(message));
|
||||||
};
|
};
|
||||||
ws.on('message', (message: string) => {
|
ws.on('message', async (message: string) => {
|
||||||
|
await lock;
|
||||||
this._dispatcherConnection.dispatch(JSON.parse(Buffer.from(message).toString()));
|
this._dispatcherConnection.dispatch(JSON.parse(Buffer.from(message).toString()));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import { PlaywrightConnection } from './playwrightConnection';
|
|||||||
import { assert } from '../utils';
|
import { assert } from '../utils';
|
||||||
import { serverSideCallMetadata } from '../server/instrumentation';
|
import { serverSideCallMetadata } from '../server/instrumentation';
|
||||||
import type { LaunchOptions } from '../server/types';
|
import type { LaunchOptions } from '../server/types';
|
||||||
|
import { ManualPromise } from '../utils/manualPromise';
|
||||||
|
|
||||||
const debugLog = debug('pw:server');
|
const debugLog = debug('pw:server');
|
||||||
|
|
||||||
@ -39,7 +40,8 @@ export type Mode = 'use-pre-launched-browser' | 'reuse-browser' | 'auto';
|
|||||||
|
|
||||||
type ServerOptions = {
|
type ServerOptions = {
|
||||||
path: string;
|
path: string;
|
||||||
maxClients: number;
|
maxIncomingConnections: number;
|
||||||
|
maxConcurrentConnections: number;
|
||||||
enableSocksProxy: boolean;
|
enableSocksProxy: boolean;
|
||||||
preLaunchedBrowser?: Browser
|
preLaunchedBrowser?: Browser
|
||||||
};
|
};
|
||||||
@ -47,7 +49,6 @@ type ServerOptions = {
|
|||||||
export class PlaywrightServer {
|
export class PlaywrightServer {
|
||||||
private _preLaunchedPlaywright: Playwright | null = null;
|
private _preLaunchedPlaywright: Playwright | null = null;
|
||||||
private _wsServer: WebSocketServer | undefined;
|
private _wsServer: WebSocketServer | undefined;
|
||||||
private _clientsCount = 0;
|
|
||||||
private _mode: Mode;
|
private _mode: Mode;
|
||||||
private _options: ServerOptions;
|
private _options: ServerOptions;
|
||||||
|
|
||||||
@ -111,10 +112,9 @@ export class PlaywrightServer {
|
|||||||
debugLog('Listening at ' + wsEndpoint);
|
debugLog('Listening at ' + wsEndpoint);
|
||||||
|
|
||||||
this._wsServer = new wsServer({ server, path: this._options.path });
|
this._wsServer = new wsServer({ server, path: this._options.path });
|
||||||
const originalShouldHandle = this._wsServer.shouldHandle.bind(this._wsServer);
|
const semaphore = new Semaphore(this._options.maxConcurrentConnections);
|
||||||
this._wsServer.shouldHandle = request => originalShouldHandle(request) && this._clientsCount < this._options.maxClients;
|
this._wsServer.on('connection', (ws, request) => {
|
||||||
this._wsServer.on('connection', async (ws, request) => {
|
if (semaphore.requested() >= this._options.maxIncomingConnections) {
|
||||||
if (this._clientsCount >= this._options.maxClients) {
|
|
||||||
ws.close(1013, 'Playwright Server is busy');
|
ws.close(1013, 'Playwright Server is busy');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -137,14 +137,14 @@ export class PlaywrightServer {
|
|||||||
if (headlessValue && headlessValue !== '0')
|
if (headlessValue && headlessValue !== '0')
|
||||||
launchOptions.headless = true;
|
launchOptions.headless = true;
|
||||||
|
|
||||||
this._clientsCount++;
|
|
||||||
const log = newLogger();
|
const log = newLogger();
|
||||||
log(`serving connection: ${request.url}`);
|
log(`serving connection: ${request.url}`);
|
||||||
const connection = new PlaywrightConnection(
|
const connection = new PlaywrightConnection(
|
||||||
|
semaphore.aquire(),
|
||||||
this._mode, ws,
|
this._mode, ws,
|
||||||
{ enableSocksProxy, browserAlias, launchOptions },
|
{ enableSocksProxy, browserAlias, launchOptions },
|
||||||
{ playwright: this._preLaunchedPlaywright, browser: this._options.preLaunchedBrowser || null },
|
{ playwright: this._preLaunchedPlaywright, browser: this._options.preLaunchedBrowser || null },
|
||||||
log, () => this._clientsCount--);
|
log, () => semaphore.release());
|
||||||
(ws as any)[kConnectionSymbol] = connection;
|
(ws as any)[kConnectionSymbol] = connection;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -174,3 +174,35 @@ export class PlaywrightServer {
|
|||||||
debugLog('closed server');
|
debugLog('closed server');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class Semaphore {
|
||||||
|
private _max: number;
|
||||||
|
private _aquired = 0;
|
||||||
|
private _queue: ManualPromise[] = [];
|
||||||
|
constructor(max: number) {
|
||||||
|
this._max = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
aquire(): Promise<void> {
|
||||||
|
const lock = new ManualPromise();
|
||||||
|
this._queue.push(lock);
|
||||||
|
this._flush();
|
||||||
|
return lock;
|
||||||
|
}
|
||||||
|
|
||||||
|
requested() {
|
||||||
|
return this._aquired + this._queue.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
release() {
|
||||||
|
--this._aquired;
|
||||||
|
this._flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _flush() {
|
||||||
|
while (this._aquired < this._max && this._queue.length) {
|
||||||
|
++this._aquired;
|
||||||
|
this._queue.shift()!.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class ManualPromise<T> extends Promise<T> {
|
export class ManualPromise<T = void> extends Promise<T> {
|
||||||
private _resolve!: (t: T) => void;
|
private _resolve!: (t: T) => void;
|
||||||
private _reject!: (e: Error) => void;
|
private _reject!: (e: Error) => void;
|
||||||
private _isDone: boolean;
|
private _isDone: boolean;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user