Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,9 @@ Visit our [contributing guide](CONTRIBUTING.md) to understand more about our dev

## License
Testplane is [MIT licensed](LICENSE).

### Общий срок WSDriver-команды

Экспериментальный режим `TESTPLANE_WSDRIVER_DEADLINE_ENABLED=true` включает общий срок WSDriver-команды: соединение, reconnect, кодирование запроса и внутренние повторы входят в `httpTimeout` (или переданный транспортом `timeout.response`). По истечении срока транспорт закрывается, дальнейшая отправка прекращается, а браузер помечается сломанным и не возвращается в кэш сессий. Ошибка `WSDRIVER_REQUEST_DEADLINE` передаётся механизму повторов тестов; внутренний повтор WebdriverIO не начинает срок заново.

Без флага сохраняются прежние таймауты и повторы команд. Закрытие незавершённого WS-соединения и отмена reconnect при закрытии работают в обоих режимах.
2 changes: 2 additions & 0 deletions src/browser/existing-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ export class ExistingBrowser extends Browser {
sessionCaps: sessionCaps as WebdriverIO.Capabilities,
headers: sessionOpts.headers as Record<string, string>,
browserConfig: this._config,
// Сессия с отменённой WS-командой не должна возвращаться в кэш.
onRequestDeadline: () => this.markAsBroken({ stubBrowserCommands: true }),
});

opts.customWdRequestAgent = this._wsDriver;
Expand Down
9 changes: 9 additions & 0 deletions src/browser/wsdriver/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,12 @@ export class WSDriverRequestError extends WsError {
return true;
}
}

export class WSDriverRequestDeadlineError extends Error {
readonly code = "WSDRIVER_REQUEST_DEADLINE";

constructor(timeout: number) {
super(`WSDriver command timed out after ${timeout}ms including connection and retries`);
this.name = "WSDriverRequestDeadlineError";
}
}
92 changes: 86 additions & 6 deletions src/browser/wsdriver/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
WSDriverError,
WSDriverRequestError,
WSDriverRequestTimeoutError,
WSDriverRequestDeadlineError,
} from "./error";
import {
WSD_ACCEPT_ENCODING_HEADER,
Expand All @@ -36,7 +37,14 @@ import { BrowserConfig } from "../../config/browser-config";
import { constructWsDriverRequest } from "./request";
import { exponentiallyWait } from "../../ws-connection/utils";

interface RequestDeadlineContext {
signal: AbortSignal;
remaining: () => number;
check: () => void;
}

interface WSDriverRequestAgentOptions {
onRequestDeadline?: () => void;
sessionId: string;
headers?: Record<string, string>;
requestTimeout: number;
Expand All @@ -50,11 +58,27 @@ export class WSDriverRequestAgent {
private _serverSupportedCompressionType?: WsDriverCompressionType;
private _sessionId: string;
private _sessionPrefix: string;
private readonly _requestTimeout: number;
private readonly _onRequestDeadline?: () => void;
private readonly _deadlineEnabled: boolean;
private readonly _requestAbort = new AbortController();

private constructor(
wsdWsEndpoint: string,
{ sessionId, headers, requestTimeout, clientSupportedCompressionTypes }: WSDriverRequestAgentOptions,
{
sessionId,
headers,
requestTimeout,
clientSupportedCompressionTypes,
onRequestDeadline,
}: WSDriverRequestAgentOptions,
) {
this._requestTimeout = requestTimeout;
this._onRequestDeadline = onRequestDeadline;
this._deadlineEnabled =
process.env.TESTPLANE_WSDRIVER_DEADLINE_ENABLED === "true" &&
Number.isFinite(requestTimeout) &&
requestTimeout > 0;
headers ||= {};
headers[WSD_ACCEPT_ENCODING_HEADER] = clientSupportedCompressionTypes.join(", ");

Expand Down Expand Up @@ -90,11 +114,13 @@ export class WSDriverRequestAgent {
sessionCaps,
headers = {},
browserConfig,
onRequestDeadline,
}: {
sessionId: string;
sessionCaps: WebdriverIO.Capabilities;
headers: Record<string, string>;
browserConfig: BrowserConfig;
onRequestDeadline?: () => void;
}): WSDriverRequestAgent {
if (!sessionCaps["se:wsdriver"]) {
throw new WSDriverError({ message: "Couldn't determine wsdriver endpoint" });
Expand Down Expand Up @@ -122,11 +148,15 @@ export class WSDriverRequestAgent {
requestTimeout,
clientSupportedCompressionTypes,
supportedVersions,
onRequestDeadline,
});
}

close(): void {
this._wsConnection.close();
if (this._deadlineEnabled) {
this._requestAbort.abort(new WSDriverRequestAgentTerminatedError());
}
this._wsConnection.close(this._deadlineEnabled);
}

private async _onMessage(data: RawData, isBinary: boolean): Promise<void> {
Expand Down Expand Up @@ -238,17 +268,64 @@ export class WSDriverRequestAgent {

/** @description Performs high-level WSDriver request with timeout */
async request(url: URL, options: RequestWsDriverOptions): Promise<RequestWsDriverResponse> {
if (!this._deadlineEnabled) return this._request(url, options);
const signal = this._requestAbort.signal;
signal.throwIfAborted();
const responseTimeout = options.timeout?.response;
const requestTimeout =
Number.isFinite(responseTimeout) && responseTimeout! > 0 ? responseTimeout! : this._requestTimeout;
const deadline = performance.now() + requestTimeout;
let onAbort!: () => void;
const expire = (): void => {
if (signal.aborted) return;
// Ошибка не ETIMEDOUT: повтор всей команды в WebdriverIO обнулит общий срок.
const error = new WSDriverRequestDeadlineError(requestTimeout);
this._requestAbort.abort(error);
this._wsConnection.close(true);
this._onRequestDeadline?.();
};
const context: RequestDeadlineContext = {
signal,
remaining: () => Math.max(1, Math.ceil(deadline - performance.now())),
check: () => {
if (performance.now() >= deadline) expire();
signal.throwIfAborted();
},
};
const aborted = new Promise<never>((_, reject) => {
onAbort = (): void => reject(signal.reason);
signal.addEventListener("abort", onAbort, { once: true });
});
const timer = setTimeout(expire, requestTimeout).unref();
try {
// Отмена закрывает транспорт; проверки после await запрещают позднюю отправку.
return await Promise.race([this._request(url, options, context), aborted]);
} finally {
clearTimeout(timer);
signal.removeEventListener("abort", onAbort);
}
}

private async _request(
url: URL,
options: RequestWsDriverOptions,
context?: RequestDeadlineContext,
): Promise<RequestWsDriverResponse> {
let requestId!: number;
let result!: IncomingWsDriverMessage | WsError;

for (let retriesLeft = WSD_REQUEST_RETRIES; retriesLeft >= 0; retriesLeft--) {
context?.check();
requestId = this._wsConnection.getRequestId();
const compressionType = await this._getRequestCompressionType();
context?.check();
const requestMessage = await constructWsDriverRequest(url, options, {
requestId,
sessionPrefix: this._sessionPrefix,
compressionType: await this._getRequestCompressionType(),
compressionType,
});

context?.check();
if (debugWSDriver.enabled) {
const header = requestMessage.readUint8(1);
const commandEndIdx = requestMessage.indexOf(0, 8);
Expand All @@ -273,10 +350,11 @@ export class WSDriverRequestAgent {
);
}

result = (await this._wsConnection.makeRequest(requestId, requestMessage).catch((err: WsError) => err)) as
| IncomingWsDriverMessage
| WsError;
result = (await this._wsConnection
.makeRequest(requestId, requestMessage, context?.remaining())
.catch((err: WsError) => err)) as IncomingWsDriverMessage | WsError;

context?.check();
if (result instanceof WSDriverRequestTimeoutError) {
const requestError = new Error(result.message);
requestError.stack = result.stack;
Expand All @@ -289,6 +367,7 @@ export class WSDriverRequestAgent {
break;
}

context?.check();
if (debugWSDriver.enabled) {
const header = requestMessage.readUint8(1);
const commandEndIdx = requestMessage.indexOf(0, 8);
Expand All @@ -310,6 +389,7 @@ export class WSDriverRequestAgent {
await exponentiallyWait({
baseDelay: WSD_REQUEST_RETRY_BASE_DELAY,
attempt: WSD_REQUEST_RETRIES - retriesLeft,
signal: context?.signal,
});
}

Expand Down
1 change: 1 addition & 0 deletions src/browser/wsdriver/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ interface IncomingWsDriverStringMessage extends IncomingWsDriverGeneralMessage {
export type IncomingWsDriverMessage = IncomingWsDriverJsonMessage | IncomingWsDriverStringMessage;

export interface RequestWsDriverOptions {
timeout?: { response: number };
path?: string;
method?: WsDriverRequestMethodString;
json?: Record<string, unknown>;
Expand Down
Loading