-
-
Notifications
You must be signed in to change notification settings - Fork 675
Feat dns interceptor storage #4589
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat dns interceptor storage #4589
Conversation
| this.#records.delete(hostname) | ||
| } | ||
|
|
||
| // Delegate to storage decide can we do more lookups or not |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example, lru-cache adapter can always return false here
| // DNS interceptor | ||
| export type DNSInterceptorRecord = { address: string, ttl: number, family: 4 | 6 } | ||
| export type DNSInterceptorOriginRecords = { 4: { ips: DNSInterceptorRecord[] } | null, 6: { ips: DNSInterceptorRecord[] } | null } | ||
| export type DNSInterceptorOriginRecords = { records: { 4: { ips: DNSInterceptorRecord[] } | null, 6: { ips: DNSInterceptorRecord[] } | null } } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here was incorrect typings - cached records shape is { records: ... }
| maxTTL?: number | ||
| maxItems?: number | ||
| lookup?: (hostname: string, options: LookupOptions, callback: (err: NodeJS.ErrnoException | null, addresses: DNSInterceptorRecord[]) => void) => void | ||
| lookup?: (origin: URL, options: LookupOptions, callback: (err: NodeJS.ErrnoException | null, addresses: DNSInterceptorRecord[]) => void) => void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here also was incorrect typings - before pick method calls, string origin is converted to URL
| const { InvalidArgumentError, InformationalError } = require('../core/errors') | ||
| const maxInt = Math.pow(2, 31) - 1 | ||
|
|
||
| class DNSStorage { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lru-cache adapter example:
const cache = new LRU({ max: maxItems })
const adapter = {
set: (hostname: string, records: any, opts: { ttl: number }): void => {
cache.set(hostname, records, opts);
},
get: (hostname: string): any => {
return cache.get(hostname);
},
delete: (hostname: string) => {
cache.delete(hostname);
},
full: (): boolean => {
return false;
},
};
const interceptor = interceptors.dns({ maxItems: Infinity, maxTTL: 60000, cache: adapter })There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, if you will be interesting about lru-cache with integrated prometheus metrics, here is our implementation:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cache metrics was a veeeery useful feature for us. But mostly for HTTP requests ofc)
| if (typeof record.ttl === 'number') { | ||
| // The record TTL is expected to be in ms | ||
| record.ttl = Math.min(record.ttl, this.#maxTTL) | ||
| minTTL = Math.min(minTTL, record.ttl) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For LRU or any other caches, which will not be able to respect ttl per record, just pass the minimum value from records.
Can't find better solution because records is a mutable object and we need to store the same for every origin,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be fine to keep it as is, this just duplicates it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea is to find minimum TTL for all resolved records for origin, and pass it to external storage, I don't quite understand about duplication
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah ok see it now, missed that bit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds custom storage support for the DNS interceptor, allowing users to provide their own cache implementation instead of relying on the built-in Map-based storage. This enables integration with external cache libraries like lru-cache and provides better control over caching behavior including custom metrics collection.
- Introduces a configurable
storageparameter for DNS interceptor that accepts custom cache implementations - Refactors internal DNS storage logic into a separate
DNSStorageclass while maintaining backward compatibility - Adds comprehensive validation for custom storage interface requirements
Reviewed Changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| lib/interceptor/dns.js | Implements the core storage abstraction with DNSStorage class and storage parameter validation |
| test/interceptors/dns.js | Adds comprehensive test coverage for external storage functionality |
| docs/docs/api/Dispatcher.md | Documents the new storage parameter with usage examples including LRU cache integration |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
|
|
||
| this.#records.set(origin.hostname, records) | ||
| // We provide a default TTL if external storage will be used without TTL per record-level support | ||
| this.storage.set(origin.hostname, records, { ttl: minTTL }) |
Copilot
AI
Sep 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The storage interface documentation shows that the ttl parameter is optional, but this code always passes it. Consider checking if the storage implementation supports TTL before passing the options parameter to avoid potential issues with storage implementations that don't expect additional parameters.
| this.storage.set(origin.hostname, records, { ttl: minTTL }) | |
| if (typeof this.storage.set === 'function' && this.storage.set.length >= 3) { | |
| this.storage.set(origin.hostname, records, { ttl: minTTL }) | |
| } else { | |
| this.storage.set(origin.hostname, records) | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The third argument is not used isn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can be used in external storages, if lru-cache or quick-lru will be used under the hood
| setRecords (origin, addresses) { | ||
| const timestamp = Date.now() | ||
| const records = { records: { 4: null, 6: null } } | ||
| let minTTL = this.#maxTTL |
Copilot
AI
Sep 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The minTTL initialization should use Infinity instead of this.#maxTTL to correctly calculate the minimum TTL across all records. With the current implementation, if all records have TTL values greater than maxTTL, minTTL will incorrectly remain at maxTTL instead of the actual minimum.
| let minTTL = this.#maxTTL | |
| let minTTL = Infinity |
| get(origin) { | ||
| return lru.get(origin); | ||
| }, | ||
| set(origin, records, { ttl }) { |
Copilot
AI
Sep 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The destructuring assignment assumes the options parameter will always be passed and contain a ttl property. This should be updated to handle cases where options might be undefined: set(origin, records, options = {}) { const { ttl } = options; ... }
| set(origin, records, { ttl }) { | |
| set(origin, records, options = {}) { | |
| const { ttl } = options; |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4589 +/- ##
=========================================
+ Coverage 0 92.94% +92.94%
=========================================
Files 0 106 +106
Lines 0 33015 +33015
=========================================
+ Hits 0 30685 +30685
- Misses 0 2330 +2330 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
docs/docs/api/Dispatcher.md
Outdated
| - The function should return a single record from the records array. | ||
| - By default a simplified version of Round Robin is used. | ||
| - The `records` property can be mutated to store the state of the balancing algorithm. | ||
| - `storage: { size: number, get(origin: string): DNSInterceptorOriginRecords | null, set(origin: string, records: DNSInterceptorOriginRecords | null, options?: { ttl: number }): void, delete(origin: string): void, full(): boolean }` - Custom storage for resolved DNS records |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's decompose it into its own section within the DNS interceptor itself
| if (typeof record.ttl === 'number') { | ||
| // The record TTL is expected to be in ms | ||
| record.ttl = Math.min(record.ttl, this.#maxTTL) | ||
| minTTL = Math.min(minTTL, record.ttl) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be fine to keep it as is, this just duplicates it.
|
|
||
| this.#records.set(origin.hostname, records) | ||
| // We provide a default TTL if external storage will be used without TTL per record-level support | ||
| this.storage.set(origin.hostname, records, { ttl: minTTL }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The third argument is not used isn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add test for the types?
Yep, ready |

Rationale
Alternative implementation for #4565
Custom
cachefor DNS interceptor allows:lru-cache, etc.)Reference - https://github.com/szmarczak/cacheable-lookup?tab=readme-ov-file#cache
Changes
Added
cacheparameter forDNSInstanceStatus