From aabb3abf9ab691fb22816aa0e33d4b379e831efd Mon Sep 17 00:00:00 2001 From: Michael Kriese <michael.kriese@visualon.de> Date: Thu, 1 Sep 2022 17:23:46 +0200 Subject: [PATCH] feat(http): use own dns cache (#17574) --- lib/util/http/dns.spec.ts | 50 ++++++++++++++ lib/util/http/dns.ts | 109 +++++++++++++++++++++++++++++-- lib/util/http/host-rules.spec.ts | 4 +- lib/util/http/host-rules.ts | 4 +- lib/workers/repository/index.ts | 3 + 5 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 lib/util/http/dns.spec.ts diff --git a/lib/util/http/dns.spec.ts b/lib/util/http/dns.spec.ts new file mode 100644 index 0000000000..b1e1477b57 --- /dev/null +++ b/lib/util/http/dns.spec.ts @@ -0,0 +1,50 @@ +import { logger } from '../../logger'; +import { clearDnsCache, dnsLookup, printDnsStats } from './dns'; + +describe('util/http/dns', () => { + describe('dnsLookup', () => { + it('works', async () => { + clearDnsCache(); + const ip = await new Promise((resolve) => + dnsLookup('api.github.com', 4, (_e, r, _f) => { + resolve(r); + }) + ); + expect(ip).toBeString(); + // uses cache + expect( + await new Promise((resolve) => + dnsLookup('api.github.com', (_e, r, _f) => { + resolve(r); + }) + ) + ).toBe(ip); + expect( + await new Promise((resolve) => + dnsLookup('api.github.com', {}, (_e, r, _f) => { + resolve(r); + }) + ) + ).toBe(ip); + }); + + it('throws', async () => { + clearDnsCache(); + const ip = new Promise((resolve, reject) => + dnsLookup('api.github.comcccccccc', 4, (_e, r, _f) => { + if (_e) { + reject(_e); + } else { + resolve(r); + } + }) + ); + await expect(ip).rejects.toThrow(); + }); + + it('prints stats', () => { + printDnsStats(); + expect(logger.debug).toHaveBeenCalled(); + }); + }); +}); diff --git a/lib/util/http/dns.ts b/lib/util/http/dns.ts index e4c8479279..df49ad448e 100644 --- a/lib/util/http/dns.ts +++ b/lib/util/http/dns.ts @@ -1,6 +1,107 @@ -import CacheableLookup from 'cacheable-lookup'; +import { LookupAllOptions, LookupOneOptions, lookup as _dnsLookup } from 'dns'; +import type { EntryObject, IPFamily, LookupOptions } from 'cacheable-lookup'; import QuickLRU from 'quick-lru'; +import { logger } from '../../logger'; -export const dnsCache = new CacheableLookup({ - cache: new QuickLRU({ maxSize: 1000 }), -}); +const cache = new QuickLRU<string, any>({ maxSize: 1000 }); + +function lookup( + ...[host, options, callback]: + | [ + hostname: string, + family: IPFamily, + callback: ( + error: NodeJS.ErrnoException, + address: string, + family: IPFamily + ) => void + ] + | [ + hostname: string, + callback: ( + error: NodeJS.ErrnoException, + address: string, + family: IPFamily + ) => void + ] + | [ + hostname: string, + options: LookupOptions & { all: true }, + callback: ( + error: NodeJS.ErrnoException, + result: ReadonlyArray<EntryObject> + ) => void + ] + | [ + hostname: string, + options: LookupOptions, + callback: ( + error: NodeJS.ErrnoException, + address: string, + family: IPFamily + ) => void + ] +): void { + let opts: LookupOneOptions | LookupAllOptions; + // TODO: strict null incompatible types (#7154) + let cb: any; + + if (typeof options === 'function') { + opts = {}; + cb = options; + } else if (typeof options === 'number') { + opts = { family: options }; + cb = callback; + } else { + opts = options; + cb = callback; + } + + // istanbul ignore if: not used + if (opts.all) { + const key = `${host}_all`; + if (cache.has(key)) { + logger.trace({ host }, 'dns lookup cache hit'); + cb(null, cache.get(key)); + return; + } + + _dnsLookup(host, opts, (err, res) => { + if (err) { + logger.error({ host, err }, 'dns lookup error'); + cb(err, null, null); + return; + } + logger.trace({ host, opts, res }, 'dns lookup'); + cache.set(key, res); + cb(null, res, null); + }); + } else { + if (cache.has(host)) { + logger.trace({ host }, 'dns lookup cache hit'); + cb(null, ...cache.get(host)); + return; + } + + _dnsLookup(host, opts, (err, ...res) => { + if (err) { + logger.error({ host, err }, 'dns lookup error'); + cb(err); + return; + } + logger.trace({ host, opts, res }, 'dns lookup'); + cache.set(host, res); + cb(null, ...res); + }); + } +} + +export { lookup as dnsLookup }; + +export function printDnsStats(): void { + logger.debug({ hosts: Array.from(cache.keys()) }, 'dns cache'); +} + +export function clearDnsCache(): void { + cache.clear(); +} diff --git a/lib/util/http/host-rules.spec.ts b/lib/util/http/host-rules.spec.ts index f640b31166..8be653b0d4 100644 --- a/lib/util/http/host-rules.spec.ts +++ b/lib/util/http/host-rules.spec.ts @@ -1,7 +1,7 @@ import { PlatformId } from '../../constants'; import { bootstrap } from '../../proxy'; import * as hostRules from '../host-rules'; -import { dnsCache } from './dns'; +import { dnsLookup } from './dns'; import { applyHostRules } from './host-rules'; const url = 'https://github.com'; @@ -114,7 +114,7 @@ describe('util/http/host-rules', () => { hostRules.add({ dnsCache: true }); expect(applyHostRules(url, { ...options, token: 'xxx' })).toMatchObject({ hostType: 'github', - dnsCache: dnsCache, + lookup: dnsLookup, token: 'xxx', }); }); diff --git a/lib/util/http/host-rules.ts b/lib/util/http/host-rules.ts index fab7ecd3b3..23310bf9d2 100644 --- a/lib/util/http/host-rules.ts +++ b/lib/util/http/host-rules.ts @@ -8,7 +8,7 @@ import { logger } from '../../logger'; import { hasProxy } from '../../proxy'; import type { HostRule } from '../../types'; import * as hostRules from '../host-rules'; -import { dnsCache } from './dns'; +import { dnsLookup } from './dns'; import type { GotOptions } from './types'; export function findMatchingRules(options: GotOptions, url: string): HostRule { @@ -106,7 +106,7 @@ export function applyHostRules(url: string, inOptions: GotOptions): GotOptions { } if (foundRules.dnsCache) { - options.dnsCache = dnsCache; + options.lookup = dnsLookup; } if (!hasProxy() && foundRules.enableHttp2 === true) { diff --git a/lib/workers/repository/index.ts b/lib/workers/repository/index.ts index 84b19b6e68..079d3ce86b 100644 --- a/lib/workers/repository/index.ts +++ b/lib/workers/repository/index.ts @@ -6,6 +6,7 @@ import { pkg } from '../../expose.cjs'; import { logger, setMeta } from '../../logger'; import { removeDanglingContainers } from '../../util/exec/docker'; import { deleteLocalFile, privateCacheDir } from '../../util/fs'; +import { clearDnsCache, printDnsStats } from '../../util/http/dns'; import * as queue from '../../util/http/queue'; import { addSplit, getSplits, splitInit } from '../../util/split'; import { setBranchCache } from './cache'; @@ -87,5 +88,7 @@ export async function renovateRepository( logger.debug(splits, 'Repository timing splits (milliseconds)'); printRequestStats(); logger.info({ durationMs: splits.total }, 'Repository finished'); + printDnsStats(); + clearDnsCache(); return repoResult; } -- GitLab