fix happy eyeballs

This commit is contained in:
Simon Knott 2024-09-14 11:42:35 +02:00
parent e17585819a
commit 95ff76a95c
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
2 changed files with 22 additions and 16 deletions

View file

@ -313,10 +313,10 @@ export abstract class APIRequestContext extends SdkObject {
const startAt = monotonicTime(); const startAt = monotonicTime();
const timings: Record<'startAt' | 'requestFinishAt' | 'dnsLookupAt' | 'tcpConnectionAt' | 'tlsHandshakeAt' | 'firstByteAt' | 'endAt', number | undefined> = { const timings: Record<'startAt' | 'requestFinishAt' | 'dnsLookupAt' | 'tcpConnectionAt' | 'tlsHandshakeAt' | 'firstByteAt' | 'endAt', number | undefined> = {
startAt, startAt,
requestFinishAt: undefined,
dnsLookupAt: undefined, dnsLookupAt: undefined,
tcpConnectionAt: undefined, tcpConnectionAt: undefined,
tlsHandshakeAt: undefined, tlsHandshakeAt: undefined,
requestFinishAt: undefined,
firstByteAt: undefined, firstByteAt: undefined,
endAt: undefined endAt: undefined
}; };
@ -326,12 +326,15 @@ export abstract class APIRequestContext extends SdkObject {
response.once('end', () => { timings.endAt = monotonicTime(); }); response.once('end', () => { timings.endAt = monotonicTime(); });
const notifyRequestFinished = (body?: Buffer) => { const notifyRequestFinished = (body?: Buffer) => {
const send = timings.requestFinishAt! - startAt; const harTimings: har.Timings = {
const dnsLookup = timings.dnsLookupAt ? startAt - timings.dnsLookupAt : undefined; send: timings.requestFinishAt! - startAt,
const tcpConnection = timings.tcpConnectionAt! - (timings.dnsLookupAt ?? startAt); wait: timings.firstByteAt! - timings.requestFinishAt!,
const tlsHandshake = timings.tlsHandshakeAt ? (timings.tlsHandshakeAt - timings.tcpConnectionAt!) : undefined; receive: timings.endAt! - timings.firstByteAt!,
const firstByte = timings.firstByteAt! - (timings.tlsHandshakeAt ?? timings.tcpConnectionAt!); dns: timings.dnsLookupAt ? timings.dnsLookupAt - startAt : -1,
const contentTransfer = timings.endAt! - timings.firstByteAt!; connect: (timings.tlsHandshakeAt ?? timings.tcpConnectionAt!) - startAt,
ssl: timings.tlsHandshakeAt ? timings.tlsHandshakeAt - timings.tcpConnectionAt! : -1,
blocked: -1, // TODO: time spent in queue waiting for a network connection
};
const requestFinishedEvent: APIRequestFinishedEvent = { const requestFinishedEvent: APIRequestFinishedEvent = {
requestEvent, requestEvent,
@ -342,15 +345,7 @@ export abstract class APIRequestContext extends SdkObject {
rawHeaders: response.rawHeaders, rawHeaders: response.rawHeaders,
cookies, cookies,
body, body,
timings: { timings: harTimings,
send,
wait: firstByte,
receive: contentTransfer,
dns: dnsLookup,
connect: tcpConnection,
ssl: tlsHandshake,
blocked: firstByte,
},
}; };
this.emit(APIRequestContext.Events.RequestFinished, requestFinishedEvent); this.emit(APIRequestContext.Events.RequestFinished, requestFinishedEvent);
}; };
@ -497,6 +492,11 @@ export abstract class APIRequestContext extends SdkObject {
request.on('close', () => this.off(APIRequestContext.Events.Dispose, disposeListener)); request.on('close', () => this.off(APIRequestContext.Events.Dispose, disposeListener));
request.on('socket', socket => { request.on('socket', socket => {
// happy eyeballs don't emit lookup and connect events, so we use our custom ones
timings.dnsLookupAt = (socket as any).dnsLookupAt;
timings.tcpConnectionAt = (socket as any).tcpConnectionAt;
// standard case
socket.on('lookup', () => { timings.dnsLookupAt = monotonicTime(); }); socket.on('lookup', () => { timings.dnsLookupAt = monotonicTime(); });
socket.on('connect', () => { timings.tcpConnectionAt = monotonicTime(); }); socket.on('connect', () => { timings.tcpConnectionAt = monotonicTime(); });
socket.on('secureConnect', () => { timings.tlsHandshakeAt = monotonicTime(); }); socket.on('secureConnect', () => { timings.tlsHandshakeAt = monotonicTime(); });

View file

@ -21,6 +21,7 @@ import * as net from 'net';
import * as tls from 'tls'; import * as tls from 'tls';
import { ManualPromise } from './manualPromise'; import { ManualPromise } from './manualPromise';
import { assert } from './debug'; import { assert } from './debug';
import { monotonicTime } from './time';
// Implementation(partial) of Happy Eyeballs 2 algorithm described in // Implementation(partial) of Happy Eyeballs 2 algorithm described in
// https://www.rfc-editor.org/rfc/rfc8305 // https://www.rfc-editor.org/rfc/rfc8305
@ -107,6 +108,7 @@ export async function createConnectionAsync(
const lookup = (options as any).__testHookLookup || lookupAddresses; const lookup = (options as any).__testHookLookup || lookupAddresses;
const hostname = clientRequestArgsToHostName(options); const hostname = clientRequestArgsToHostName(options);
const addresses = await lookup(hostname); const addresses = await lookup(hostname);
const dnsLookupAt = monotonicTime();
const sockets = new Set<net.Socket>(); const sockets = new Set<net.Socket>();
let firstError; let firstError;
let errorCount = 0; let errorCount = 0;
@ -132,9 +134,13 @@ export async function createConnectionAsync(
port: options.port as number, port: options.port as number,
host: address }); host: address });
(socket as any).dnsLookupAt = dnsLookupAt;
// Each socket may fire only one of 'connect', 'timeout' or 'error' events. // Each socket may fire only one of 'connect', 'timeout' or 'error' events.
// None of these events are fired after socket.destroy() is called. // None of these events are fired after socket.destroy() is called.
socket.on('connect', () => { socket.on('connect', () => {
(socket as any).tcpConnectionAt = monotonicTime();
connected.resolve(); connected.resolve();
oncreate?.(null, socket); oncreate?.(null, socket);
// TODO: Cache the result? // TODO: Cache the result?