From 2f80a536c39d767775a03e76c7dfdbde71bcb704 Mon Sep 17 00:00:00 2001 From: fallenbagel <98979876+Fallenbagel@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:53:12 +0800 Subject: [PATCH] feat: simple implementation of dnscaching --- server/utils/dnsCacheManager.ts | 122 ++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 server/utils/dnsCacheManager.ts diff --git a/server/utils/dnsCacheManager.ts b/server/utils/dnsCacheManager.ts new file mode 100644 index 000000000..70138b3fd --- /dev/null +++ b/server/utils/dnsCacheManager.ts @@ -0,0 +1,122 @@ +import logger from '@server/logger'; +import { LRUCache } from 'lru-cache'; +import dns from 'node:dns'; + +interface DnsCache { + address: string; + family: number; + timestamp: number; +} + +interface CacheStats { + hits: number; + misses: number; +} + +class DnsCacheManager { + private cache: LRUCache; + private lookupAsync: typeof dns.promises.lookup; + private stats: CacheStats = { + hits: 0, + misses: 0, + }; + + constructor(ttlMs = 300000) { + this.cache = new LRUCache({ max: 500, ttl: ttlMs }); + this.lookupAsync = dns.promises.lookup; + } + + async lookup(hostname: string): Promise { + const cached = this.cache.get(hostname); + if (cached) { + this.stats.hits++; + logger.debug(`DNS cache hit for ${hostname}`, { + label: 'DNSCache', + address: cached.address, + family: cached.family, + age: Date.now() - cached.timestamp, + }); + return cached; + } + + this.stats.misses++; + try { + const result = await this.lookupAsync(hostname); + const dnsCache: DnsCache = { + address: result.address, + family: result.family, + timestamp: Date.now(), + }; + + this.cache.set(hostname, dnsCache); + logger.debug(`DNS cache miss for ${hostname}, cached new result`, { + label: 'DNSCache', + address: dnsCache.address, + family: dnsCache.family, + }); + return dnsCache; + } catch (error) { + throw new Error(`DNS lookup failed for ${hostname}: ${error.message}`); + } + } + + getStats() { + const entries = [...this.cache.entries()]; + return { + size: entries.length, + maxSize: this.cache.max, + hits: this.stats.hits, + misses: this.stats.misses, + hitRate: this.stats.hits / (this.stats.hits + this.stats.misses || 1), + }; + } + + getCacheEntries() { + const entries: Record< + string, + { + address: string; + family: number; + age: number; + ttl: number; + } + > = {}; + + for (const [hostname, data] of this.cache.entries()) { + const age = Date.now() - data.timestamp; + const ttl = (this.cache.ttl ?? 300000) - age; + + entries[hostname] = { + address: data.address, + family: data.family, + age, + ttl, + }; + } + + return entries; + } + + getCacheEntry(hostname: string) { + const entry = this.cache.get(hostname); + if (!entry) { + return null; + } + + return { + address: entry.address, + family: entry.family, + age: Date.now() - entry.timestamp, + ttl: (this.cache.ttl ?? 300000) - (Date.now() - entry.timestamp), + }; + } + + clear() { + this.cache.clear(); + this.stats.hits = 0; + this.stats.misses = 0; + logger.debug('DNS cache cleared', { label: 'DNSCache' }); + } +} + +export const dnsCache = new DnsCacheManager();