Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
48 changes: 48 additions & 0 deletions benchmarks/fetch/sequential-keepalive.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict'

import { createServer } from 'node:http'
import { Agent, fetch } from '../../index.js'

const ITERATIONS = Number(process.env.SAMPLES ?? 2000)
const WARMUP = 300

const server = createServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' })
res.end('{"ok":1}')
})

server.keepAliveTimeout = 65_000

await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))

const { port } = server.address()
const url = `http://127.0.0.1:${port}/`
const agent = new Agent({
keepAliveTimeout: 60_000,
connections: 1,
pipelining: 1
})

for (let i = 0; i < WARMUP; i++) {
await (await fetch(url, { dispatcher: agent })).text()
}

const times = new Array(ITERATIONS)
for (let i = 0; i < ITERATIONS; i++) {
const t0 = process.hrtime.bigint()
await (await fetch(url, { dispatcher: agent })).text()
times[i] = Number(process.hrtime.bigint() - t0)
}

times.sort((a, b) => a - b)
const pct = (p) => times[Math.min(ITERATIONS - 1, Math.floor(ITERATIONS * p))] / 1e6

console.log(JSON.stringify({
iterations: ITERATIONS,
p50_ms: Number(pct(0.5).toFixed(3)),
p90_ms: Number(pct(0.9).toFixed(3)),
p99_ms: Number(pct(0.99).toFixed(3))
}))

await agent.close()
server.close()
16 changes: 12 additions & 4 deletions lib/dispatcher/client-h1.js
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,7 @@ function onSocketClose () {

function clearIdleSocketValidation (socket) {
if (socket[kIdleSocketValidationTimeout]) {
clearTimeout(socket[kIdleSocketValidationTimeout])
clearImmediate(socket[kIdleSocketValidationTimeout])
socket[kIdleSocketValidationTimeout] = null
}

Expand All @@ -1061,15 +1061,23 @@ function clearIdleSocketValidation (socket) {

function scheduleIdleSocketValidation (client, socket) {
socket[kIdleSocketValidation] = 1
socket[kIdleSocketValidationTimeout] = setTimeout(() => {
// Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
// already pending on this idle keep-alive socket are processed before the
// next request is written (GHSA-35p6-xmwp-9g52).
//
// setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
// (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
// block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
// A ref'd Immediate both keeps the pending request alive and makes poll
// return immediately — the hybrid those issues asked for.
socket[kIdleSocketValidationTimeout] = setImmediate(() => {
socket[kIdleSocketValidationTimeout] = null
socket[kIdleSocketValidation] = 2

if (client[kSocket] === socket && !socket.destroyed) {
client[kResume]()
}
}, 0)
socket[kIdleSocketValidationTimeout].unref?.()
})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please move these to an independent pr

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the idle-socket setImmediate change to #5707.

}

/**
Expand Down
5 changes: 2 additions & 3 deletions lib/web/fetch/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -923,7 +923,7 @@ function makeRequest (init) {
serviceWorkers: init.serviceWorkers ?? 'all',
initiator: init.initiator ?? '',
destination: init.destination ?? '',
priority: init.priority ?? null,
priority: init.priority ?? 'auto',
origin: init.origin ?? 'client',
policyContainer: init.policyContainer ?? 'client',
referrer: init.referrer ?? 'client',
Expand Down Expand Up @@ -1129,8 +1129,7 @@ webidl.converters.RequestInit = webidl.dictionaryConverter([
{
key: 'priority',
converter: webidl.converters.DOMString,
allowedValues: ['high', 'low', 'auto'],
defaultValue: () => 'auto'
allowedValues: ['high', 'low', 'auto']
}
])

Expand Down
48 changes: 47 additions & 1 deletion test/node-test/keep-alive-reuse.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const { test } = require('node:test')
const assert = require('node:assert')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { Pool } = require('../..')
const { Agent, Pool, fetch } = require('../..')

// Regression for #5600 / #5606:
// Reusing an idle keep-alive socket must not stall behind the poll phase.
Expand Down Expand Up @@ -66,3 +66,49 @@ test('reusing an idle keep-alive socket must not stall', { timeout: 1000 }, asyn
assert.strictEqual(connections, 1, 'keep-alive socket must be reused')
}
})

test('fetch reusing an idle keep-alive socket must not stall', { timeout: 1000 }, async (t) => {
let connections = 0

const server = createServer((req, res) => {
res.writeHead(200, { 'content-length': 2 })
res.end('ok')
})

server.on('connection', () => {
connections++
})

server.listen(0)
await once(server, 'listening')

const url = `http://127.0.0.1:${server.address().port}`
const agent = new Agent({
connections: 1,
pipelining: 1,
keepAliveTimeout: 60_000
})

t.after(async () => {
await agent.close()
server.close()
})

{
const res = await fetch(`${url}/0`, { dispatcher: agent })
assert.strictEqual(await res.text(), 'ok')
}
assert.strictEqual(connections, 1)

for (let i = 1; i <= REUSES; i++) {
const requested = once(server, 'request')
const resPromise = fetch(`${url}/${i}`, { dispatcher: agent })
resPromise.catch(() => {})

await requested

const res = await resPromise
assert.strictEqual(await res.text(), 'ok')
assert.strictEqual(connections, 1, 'keep-alive socket must be reused')
}
})
Loading