-
Notifications
You must be signed in to change notification settings - Fork 570
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: add dns cache #2440 #2552
Conversation
Note that this is just an initial draft, anyone can jump in and use this as a base or if in wrong direction simply discard 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.
I would create a new class DNSResolver and add a dnsResolver
option to pool and Agent. An Agent or pool would allocate one if it's not passed in as an option. The Agent would pass down its cache to all created pools.
Benchmarks:
main branch 9b9ca60
Feature branch:
|
lib/core/dns-resolver.js
Outdated
if (entries[0].status === 'rejected' && entries[1].status === 'rejected') { | ||
throw entries[0].reason | ||
} |
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.
Why only throw the first error? Wouldn't it be better to throw an AggregateError that contains both?
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.
doing that, using AggregateError and modifying tests
lib/core/dns-resolver.js
Outdated
@@ -0,0 +1,443 @@ | |||
// source: https://raw.githubusercontent.com/szmarczak/cacheable-lookup/9e60c9f6e74a003692aec68f3ddad93afe613b8f/source/index.mjs |
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.
please include the license at the top of the file
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.
yes no probs
lib/core/dns-resolver.js
Outdated
) | ||
cacheError.cause = error | ||
|
||
throw cacheError |
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.
I don't think this kind of error handling is correct. Error situations should auto heal when possible
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 it is because it's possible to specify a custom cache object, if the set method throws it is considered as a fatal and unrecoverable error.
Using the default cache which is a simple Map() this would not happen.
Also happy to remove any form of custom caching, that's was part of the copied module.
lib/core/dns-resolver.js
Outdated
const { _iface } = this | ||
cached = cached.filter((entry) => | ||
entry.family === 6 ? _iface.has6 : _iface.has4 | ||
) |
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.
Please avoid the use of filter, map etc, they are an order of magnitude (or more) slower than using normal iteration.
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.
I translated the code with loops, no optimizations yet. To have the code passing all tests. Made the function less readable. Also note that here it will be common to have one or two items returns and I am yet to see anyone using more than 4-5 items per record.
Agreeing to keep an eye on the perf side.
Update:
|
I don't think there should be a global dns resolver. We already have a global agent, which is enough to handle this use case. |
Codecov ReportAttention:
Additional details and impacted files@@ Coverage Diff @@
## main #2552 +/- ##
==========================================
+ Coverage 85.54% 85.56% +0.02%
==========================================
Files 76 85 +9
Lines 6858 7810 +952
==========================================
+ Hits 5867 6683 +816
- Misses 991 1127 +136 ☔ View full report in Codecov by Sentry. |
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 we add tests that covers the usage within the Agent/Pool?
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.
I forgot with my other review but this does need types as well.
lib/dns-resolver.js
Outdated
get _cache () { | ||
return this.#cache | ||
} |
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.
from the tests, it looks like this is only needed to check how many entries are in the cache. Would it make sense to only expose the number of cached entries, rather than the cache itself? If not, changing these to a symbol property to deter users from using this would be better.
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.
Yes agree
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.
Done
lib/dns-resolver.js
Outdated
get _hostnamesToFallback () { | ||
return this.#hostnamesToFallback | ||
} |
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.
ditto
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.
Done
Co-authored-by: Carlos Fuentes <me@metcoder.dev>
Co-authored-by: Carlos Fuentes <me@metcoder.dev>
Co-authored-by: Carlos Fuentes <me@metcoder.dev>
I did add two tests, one that will check that the cache is incrementing when using the DNS resolver and one that check that no cache is being used if disabled. Since Agent is not exposing the DNSResolver it is not possible to get it and check the cache, but I will have a look for a solution to see if I can intercept/mock it. |
linting is failing |
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.
We would need to implement some kind of solution of "Happy Eyeballs" similar to what Node Core did.
The more I look at this, the more I'm thinking this should live in Node.js core and not here, but let's chat about it.
README.md
Outdated
* **hints** [`getaddrinfo flags`](https://nodejs.org/api/dns.html#supported-getaddrinfo-flags) | ||
* **all** `Boolean` - Default: `false` | ||
|
||
* **roundRobinStrategy** `'first' | 'random'` - Default: `'first'` |
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.
this should be called scheduling
. What does first
vs random
do?
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.
done
} | ||
|
||
if (this.roundRobinStrategy === 'first') { | ||
return cached[0] |
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.
if we do not rotate the cache, then it's not round robin.
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.
yes true, I will think of a way to rotate the cache
first time I add types in non typescript project, is that ok? |
index.js
Outdated
@@ -165,3 +165,6 @@ module.exports.MockClient = MockClient | |||
module.exports.MockPool = MockPool | |||
module.exports.MockAgent = MockAgent | |||
module.exports.mockErrors = mockErrors | |||
|
|||
const DNSResolver = require('./lib/dns-resolver') |
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.
Move this to the top
cc @ShogunPanda |
return cached | ||
} | ||
|
||
if (this.#scheduling === 'first') { |
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.
Wondering, what are the intentions with the scheduling
option?
Its not added within cacheable-lookup
as it already caches the 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.
So with first
it is the same behavior as cacheable-lookup
and returns the first cached entry from cache.
With random
it will return a random item from the cache.
This is a way to achieve some sort of round robin without to much complexity.
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.
Minor suggestions, but not blockers.
LGTM!
lib/agent.js
Outdated
@@ -22,6 +23,18 @@ function defaultFactory (origin, opts) { | |||
: new Pool(origin, opts) | |||
} | |||
|
|||
function getDNSResolver (options) { | |||
if (options?.dnsResolverOptions?.disable) { |
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.
I wonder if it would be a better DX to have { dnsResolverOptions: false }
instead to completely disable it.
Not a blocker, just a thought.
types/agent.d.ts
Outdated
@@ -22,6 +23,8 @@ declare namespace Agent { | |||
maxRedirections?: number; | |||
|
|||
interceptors?: { Agent?: readonly Dispatcher.DispatchInterceptor[] } & Pool.Options["interceptors"] | |||
|
|||
dnsResolverOptions: DNSResolver.Options; |
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.
Similarly, what about having just dnsResolver
here?
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 was asked before to rename the options to dnsResolverOptions
and have a separate one called dnsResolver
to specify a custom DNSResolver class.
I changed the disable check on dnsResolver
instead of putting it in the options.
So to disable it will be:
new Agent({
dnsResolver: false,
})
In regard of Happy Eyeballs, let me clarify our options here. From what I see now, with this new feature undici will attempt the records in the order node (and thus the DNS server) resolves them). This is a good thing, at least as default. The next step would be to implement Happy Eyeballs. I would add a specific implementation in undici since we can be explicit and control the sockets we open and thus we can implement the full Happy Eyeballs specification. In other words, in Node I was forced to implement a serialized version of the algorithm (not to break too many things), here we don't have to. If we do this, we have two options:
|
I think we would need happy eyeballs to enable this by default. |
We should also not forget to add an option to disable it if required
I'd like to see if we can keep the
Like this pretty much, do you think this can enable features such as #2515? |
Absolutely yes!
I'm fine with it. Maybe a separate class used by the the
Probably yes, and I would love it! |
types/pool.d.ts
Outdated
interceptors?: { Pool?: readonly Dispatcher.DispatchInterceptor[] } & Client.Options["interceptors"] | ||
interceptors?: { Pool?: readonly Dispatcher.DispatchInterceptor[] } & Client.Options["interceptors"]; | ||
|
||
dnsResolver?: typeof DNSResolver; |
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.
please add tests for this types
I added some tests for types, tried to resolve your comments. For the Happy Eyeballs I would be glad to do it too if that's ok. Any source or link to existing material? Since I changed the behavior for this to be disabled by default it can be done in another PR. |
Yeah, another PR will be good. So far I believe this is the implementation: https://github.com/ShogunPanda/node/blob/ffb326c583c6a23bc94ff4989fe8a06edb92b011/lib/net.js#L1401 and this is the standard that is based on: https://datatracker.ietf.org/doc/html/rfc8305 Are those resources all right @ShogunPanda? As pointed out, we should seek to stick to the spec as much as we can as we have better ergonomics here to do so. |
@metcoder95 Yes, all good links sir. :) |
Hello @ShogunPanda, I have read the RFC and also the code linked. Some thoughts. For now this PR is creating a custom lookup function to cache dns queries. By default the options for lookup are In the RFC it suggest that we should not wait for all DNS queries to resolve to start connecting. Starting to query ipv6 then ipv4. Would be a bit more complex to make it work. I also understand that we could implement the happy eye balls in undici using a similar approach than node/net module but since we are relying on it I am wondering if it is worth it. |
In Node.js it is going to use Happy Eyeballs only if using a hostname when doing |
Great to see work on this, we are very interested in the possibility of DNS caching as in kubernetes in particular the DNS lookups become a big issue at scale (even with local caching) |
@antoinegomez could I help with that pr in any way ? |
Closing due to staleness. |
This relates to #2440
Rationale
Implement a dns cache lookup feature as requested.
Copy code from https://github.com/szmarczak/cacheable-lookup/blob/master/source/index.mjs
Changes
Features
Bug Fixes
N/A
Breaking Changes and Deprecations
N/A
Status