diff --git a/lib/client.js b/lib/client.js index 496c568d732..983e7034795 100644 --- a/lib/client.js +++ b/lib/client.js @@ -5,6 +5,7 @@ const assert = require('assert') const net = require('net') const util = require('./core/util') +const timers = require('./timers') const Request = require('./core/request') const DispatcherBase = require('./dispatcher-base') const { @@ -444,9 +445,9 @@ class Parser { setTimeout (value, type) { this.timeoutType = type if (value !== this.timeoutValue) { - clearTimeout(this.timeout) + timers.clearTimeout(this.timeout) if (value) { - this.timeout = setTimeout(onParserTimeout, value, this) + this.timeout = timers.setTimeout(onParserTimeout, value, this) // istanbul ignore else: only for jest if (this.timeout.unref) { this.timeout.unref() @@ -562,7 +563,7 @@ class Parser { this.llhttp.llhttp_free(this.ptr) this.ptr = null - clearTimeout(this.timeout) + timers.clearTimeout(this.timeout) this.timeout = null this.timeoutValue = null this.timeoutType = null diff --git a/lib/timers.js b/lib/timers.js new file mode 100644 index 00000000000..f96bc62f286 --- /dev/null +++ b/lib/timers.js @@ -0,0 +1,89 @@ +'use strict' + +let fastNow = Date.now() +let fastNowTimeout + +const fastTimers = [] + +function onTimeout () { + fastNow = Date.now() + + let len = fastTimers.length + let idx = 0 + while (idx < len) { + const timer = fastTimers[idx] + + if (timer.expires && fastNow >= timer.expires) { + timer.expires = 0 + timer.callback(timer.opaque) + } + + if (timer.expires === 0) { + timer.active = false + if (idx !== len - 1) { + fastTimers[idx] = fastTimers.pop() + } else { + fastTimers.pop() + } + len -= 1 + } else { + idx += 1 + } + } + + if (fastTimers.length > 0) { + refreshTimeout() + } +} + +function refreshTimeout () { + if (fastNowTimeout && fastNowTimeout.refresh) { + fastNowTimeout.refresh() + } else { + clearTimeout(fastNowTimeout) + fastNowTimeout = setTimeout(onTimeout, 1e3) + if (fastNowTimeout.unref) { + fastNowTimeout.unref() + } + } +} + +class Timeout { + constructor (callback, delay, opaque) { + this.callback = callback + this.delay = delay + this.opaque = opaque + this.expires = 0 + this.active = false + + this.refresh() + } + + refresh () { + if (!this.active) { + this.active = true + fastTimers.push(this) + if (!fastNowTimeout || fastTimers.length === 1) { + refreshTimeout() + fastNow = Date.now() + } + } + + this.expires = fastNow + this.delay + } + + clear () { + this.expires = 0 + } +} + +module.exports = { + setTimeout (callback, delay, opaque) { + return new Timeout(callback, delay, opaque) + }, + clearTimeout (timeout) { + if (timeout && timeout.clear) { + timeout.clear() + } + } +} diff --git a/test/client-keep-alive.js b/test/client-keep-alive.js index b0f81606067..5360675c65c 100644 --- a/test/client-keep-alive.js +++ b/test/client-keep-alive.js @@ -2,6 +2,7 @@ const { test } = require('tap') const { Client } = require('..') +const timers = require('../lib/timers') const { kConnect } = require('../lib/core/symbols') const { createServer } = require('net') const http = require('http') @@ -47,6 +48,12 @@ test('keep-alive header 0', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((socket) => { socket.write('HTTP/1.1 200 OK\r\n') socket.write('Content-Length: 0\r\n') diff --git a/test/client-reconnect.js b/test/client-reconnect.js index 6abb8e403f3..ae1a206de63 100644 --- a/test/client-reconnect.js +++ b/test/client-reconnect.js @@ -4,6 +4,7 @@ const { test } = require('tap') const { Client } = require('..') const { createServer } = require('http') const FakeTimers = require('@sinonjs/fake-timers') +const timers = require('../lib/timers') test('multiple reconnect', (t) => { t.plan(5) @@ -12,6 +13,12 @@ test('multiple reconnect', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { n === 0 ? res.destroy() : res.end('ok') }) diff --git a/test/client-timeout.js b/test/client-timeout.js index eedf1f48c97..5f1686a0251 100644 --- a/test/client-timeout.js +++ b/test/client-timeout.js @@ -5,6 +5,7 @@ const { Client, errors } = require('..') const { createServer } = require('http') const { Readable } = require('stream') const FakeTimers = require('@sinonjs/fake-timers') +const timers = require('../lib/timers') test('refresh timeout on pause', (t) => { t.plan(1) @@ -51,6 +52,12 @@ test('start headers timeout after request body', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { }) t.teardown(server.close.bind(server)) @@ -101,6 +108,12 @@ test('start headers timeout after async iterator request body', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { }) t.teardown(server.close.bind(server)) @@ -167,7 +180,7 @@ test('parser resume with no body timeout', (t) => { onConnect () { }, onHeaders (statusCode, headers, resume) { - setTimeout(resume, 100) + setTimeout(resume, 2000) return false }, onData () { diff --git a/test/fetch/fetch-timeouts.js b/test/fetch/fetch-timeouts.js index adbf888ebba..b659aaa08d6 100644 --- a/test/fetch/fetch-timeouts.js +++ b/test/fetch/fetch-timeouts.js @@ -3,6 +3,7 @@ const { test } = require('tap') const { fetch, Agent } = require('../..') +const timers = require('../../lib/timers') const { createServer } = require('http') const FakeTimers = require('@sinonjs/fake-timers') @@ -16,6 +17,12 @@ test('Fetch very long request, timeout overridden so no error', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') diff --git a/test/request-timeout.js b/test/request-timeout.js index 2d2e826acac..972ebd0b373 100644 --- a/test/request-timeout.js +++ b/test/request-timeout.js @@ -4,6 +4,7 @@ const { test } = require('tap') const { createReadStream, writeFileSync, unlinkSync } = require('fs') const { Client, errors } = require('..') const { kConnect } = require('../lib/core/symbols') +const timers = require('../lib/timers') const { createServer } = require('http') const EventEmitter = require('events') const FakeTimers = require('@sinonjs/fake-timers') @@ -65,6 +66,12 @@ test('body timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { res.write('hello') }) @@ -93,6 +100,12 @@ test('overridden request timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -119,6 +132,12 @@ test('overridden body timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { res.write('hello') }) @@ -147,6 +166,12 @@ test('With EE signal', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -176,6 +201,12 @@ test('With abort-controller signal', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -205,6 +236,12 @@ test('Abort before timeout (EE)', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const ee = new EventEmitter() const server = createServer((req, res) => { setTimeout(() => { @@ -234,6 +271,12 @@ test('Abort before timeout (abort-controller)', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const abortController = new AbortController() const server = createServer((req, res) => { setTimeout(() => { @@ -263,6 +306,12 @@ test('Timeout with pipelining', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -298,6 +347,12 @@ test('Global option', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -326,6 +381,12 @@ test('Request options overrides global option', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -377,6 +438,12 @@ test('client.close should wait for the timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { }) t.teardown(server.close.bind(server)) @@ -449,6 +516,12 @@ test('Disable request timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -485,6 +558,12 @@ test('Disable request timeout for a single request', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -521,6 +600,12 @@ test('stream timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -551,6 +636,12 @@ test('stream custom timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -583,6 +674,12 @@ test('pipeline timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { req.pipe(res) @@ -632,6 +729,12 @@ test('pipeline timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { req.pipe(res) @@ -683,6 +786,12 @@ test('client.close should not deadlock', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { }) t.teardown(server.close.bind(server)) diff --git a/test/socket-timeout.js b/test/socket-timeout.js index 6ce58369643..8019c74198a 100644 --- a/test/socket-timeout.js +++ b/test/socket-timeout.js @@ -2,6 +2,7 @@ const { test } = require('tap') const { Client, errors } = require('..') +const timers = require('../lib/timers') const { createServer } = require('http') const FakeTimers = require('@sinonjs/fake-timers') @@ -64,6 +65,12 @@ test('Disable socket timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + server.once('request', (req, res) => { setTimeout(() => { res.end('hello')