Skip to content

Commit

Permalink
Introduce bun.dns.prefetch API (#11176)
Browse files Browse the repository at this point in the history
Co-authored-by: Jarred-Sumner <[email protected]>
Co-authored-by: Eric L. Goldstein <[email protected]>
  • Loading branch information
3 people authored May 20, 2024
1 parent 16e0f6e commit b15d47d
Show file tree
Hide file tree
Showing 18 changed files with 757 additions and 140 deletions.
38 changes: 38 additions & 0 deletions bench/snippets/dns-prefetch.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// For maximum effect, make sure to clear your DNS cache before running this
//
// To clear your DNS cache on macOS:
// sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder
//
// To clear your DNS cache on Linux:
// sudo systemd-resolve --flush-caches && sudo killall -HUP systemd-resolved
//
// To clear your DNS cache on Windows:
// ipconfig /flushdns
//
const url = new URL(process.argv.length > 2 ? process.argv.at(-1) : "https://bun.sh");
const hostname = url.hostname;
const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;

if (typeof globalThis.Bun?.dns?.prefetch === "function") {
Bun.dns.prefetch(hostname, port);
}

// Delay one second to make sure the DNS prefetch has time to run
await new Promise(resolve => setTimeout(resolve, 1000));

const start = performance.now();
const promises = [];

// Now let's fetch 20 times to see if the DNS prefetch has worked
for (let i = 0; i < 20; i++) {
promises.push(fetch(url, { redirect: "manual", method: "HEAD" }));
}

await Promise.all(promises);

const end = performance.now();
console.log("fetch() took", (end - start) | 0, "ms");

if (typeof globalThis.Bun?.dns?.getCacheStats === "function") {
console.log("DNS cache stats", Bun.dns.getCacheStats());
}
120 changes: 96 additions & 24 deletions docs/api/dns.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,106 @@ console.log(addrs);
// => [{ address: "172.67.161.226", family: 4, ttl: 0 }, ...]
```

<!--
## `Bun.dns` - lookup a domain
`Bun.dns` includes utilities to make DNS requests, similar to `node:dns`. As of Bun v0.5.0, the only implemented function is `dns.lookup`, though more will be implemented soon.
You can lookup the IP addresses of a hostname by using `dns.lookup`.
## DNS caching in Bun

In Bun v1.1.9, we added support for DNS caching. This cache makes repeated connections to the same hosts faster.

At the time of writing, we cache up to 255 entries for a maximum of 30 seconds (each). If any connections to a host fail, we remove the entry from the cache. When multiple connections are made to the same host simultaneously, DNS lookups are deduplicated to avoid making multiple requests for the same host.

This cache is automatically used by;

- `bun install`
- `fetch()`
- `node:http` (client)
- `Bun.connect`
- `node:net`
- `node:tls`

### When should I prefetch a DNS entry?

Web browsers expose [`<link rel="dns-prefetch">`](https://developer.mozilla.org/en-US/docs/Web/Performance/dns-prefetch) to allow developers to prefetch DNS entries. This is useful when you know you'll need to connect to a host in the near future and want to avoid the initial DNS lookup.

In Bun, you can use the `dns.prefetch` API to achieve the same effect.

```ts
import { dns } from "bun";
const [{ address }] = await dns.lookup("example.com");
console.log(address); // "93.184.216.34"
import {dns} from "bun";

dns.prefetch("my.database-host.com", 5432);
```
If you need to limit IP addresses to either IPv4 or IPv6, you can specify the `family` as an option.

An example where you might want to use this is a database driver. When your application first starts up, you can prefetch the DNS entry for the database host so that by the time it finishes loading everything, the DNS query to resolve the database host may already be completed.

### `dns.prefetch`

{% callout %}
**🚧** — This API is experimental and may change in the future.
{% /callout %}

To prefetch a DNS entry, you can use the `dns.prefetch` API. This API is useful when you know you'll need to connect to a host soon and want to avoid the initial DNS lookup.

```ts
import { dns } from "bun";
const [{ address }] = await dns.lookup("example.com", { family: 6 });
console.log(address); // "2606:2800:220:1:248:1893:25c8:1946"
dns.prefetch(hostname: string, port: number): void;
```
Bun supports three backends for DNS resolution:
- `c-ares` - This is the default on Linux, and it uses the [c-ares](https://c-ares.org/) library to perform DNS resolution.
- `system` - Uses the system's non-blocking DNS resolver, if available. Otherwise, falls back to `getaddrinfo`. This is the default on macOS, and the same as `getaddrinfo` on Linux.
- `getaddrinfo` - Uses the POSIX standard `getaddrinfo` function, which may cause performance issues under concurrent load.

You can choose a particular backend by specifying `backend` as an option.
Here's an example:

```ts
import { dns } from "bun";
const [{ address, ttl }] = await dns.lookup("example.com", {
backend: "c-ares"
});
console.log(address); // "93.184.216.34"
console.log(ttl); // 21237
import {dns} from "bun";

dns.prefetch("bun.sh", 443);
//
// ... sometime later ...
await fetch("https://bun.sh");
```
Note: the `ttl` property is only accurate when the `backend` is c-ares. Otherwise, `ttl` will be `0`.
This was added in Bun v0.5.0. -->

### `dns.getCacheStats()`

{% callout %}
**🚧** — This API is experimental and may change in the future.
{% /callout %}

To get the current cache stats, you can use the `dns.getCacheStats` API.

This API returns an object with the following properties:

```ts
{
// Cache hits
cacheHitsCompleted: number;
cacheHitsInflight: number;
cacheMisses: number;
// Number of items in the DNS cache
size: number;

// Number of times a connection failed
errors: number;

// Number of times a connection was requested at all (including cache hits and misses)
totalCount: number;
}
```

Example:

```ts
import {dns} from "bun";

const stats = dns.getCacheStats();
console.log(stats);
// => { cacheHitsCompleted: 0, cacheHitsInflight: 0, cacheMisses: 0, size: 0, errors: 0, totalCount: 0 }
```

### Configuring DNS cache TTL

Bun defaults to 30 seconds for the TTL of DNS cache entries. To change this, you can set the envionrment variable `$BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS`. For example, to set the TTL to 5 seconds:

```sh
BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS=5 bun run my-script.ts
```

#### Why is 30 seconds the default?

Unfortunately, the system API underneath (`getaddrinfo`) does not provide a way to get the TTL of a DNS entry. This means we have to pick a number arbitrarily. We chose 30 seconds because it's long enough to see the benefits of caching, and short enough to be unlikely to cause issues if a DNS entry changes. [Amazon Web Services recommends 5 seconds](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/jvm-ttl-dns.html) for the Java Virtual Machine, however the JVM defaults to cache indefinitely.



36 changes: 36 additions & 0 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,42 @@ declare module "bun" {
backend?: "libc" | "c-ares" | "system" | "getaddrinfo";
},
): Promise<DNSLookup[]>;

/**
*
* **Experimental API**
*
* Prefetch a hostname and port.
*
* This will be used by fetch() and Bun.connect() to avoid DNS lookups.
*
* @param hostname The hostname to prefetch
* @param port The port to prefetch
*
* @example
* ```js
* import { dns } from 'bun';
* dns.prefetch('example.com', 443);
* // ... something expensive
* await fetch('https://example.com');
* ```
*/
prefetch(hostname: string, port: number): void;

/**
* **Experimental API**
*/
getCacheStats(): {
/**
* The number of times a cached DNS entry that was already resolved was used.
*/
cacheHitsCompleted: number;
cacheHitsInflight: number;
cacheMisses: number;
size: number;
errors: number;
totalCount: number;
};
};

interface DNSLookup {
Expand Down
80 changes: 70 additions & 10 deletions packages/bun-usockets/src/context.c
Original file line number Diff line number Diff line change
Expand Up @@ -344,13 +344,60 @@ struct us_listen_socket_t *us_socket_context_listen_unix(int ssl, struct us_sock
return ls;
}

struct us_connecting_socket_t *us_socket_context_connect(int ssl, struct us_socket_context_t *context, const char *host, int port, int options, int socket_ext_size) {

struct us_socket_t* us_socket_context_connect_resolved_dns(struct us_socket_context_t *context, void* request, int options, int socket_ext_size) {
struct addrinfo_result *result = Bun__addrinfo_getRequestResult(request);
if (result->error) {
errno = result->error;
Bun__addrinfo_freeRequest(request, 1);
return NULL;
}

LIBUS_SOCKET_DESCRIPTOR connect_socket_fd = bsd_create_connect_socket(result->info, options);
if (connect_socket_fd == LIBUS_SOCKET_ERROR) {
int err = errno;
Bun__addrinfo_freeRequest(request, err);
return NULL;
}

Bun__addrinfo_freeRequest(request, 0);
bsd_socket_nodelay(connect_socket_fd, 1);

/* Connect sockets are semi-sockets just like listen sockets */
struct us_poll_t *p = us_create_poll(context->loop, 0, sizeof(struct us_socket_t) + socket_ext_size);
us_poll_init(p, connect_socket_fd, POLL_TYPE_SEMI_SOCKET);
us_poll_start(p, context->loop, LIBUS_SOCKET_WRITABLE);

struct us_socket_t *socket = (struct us_socket_t *) p;

/* Link it into context so that timeout fires properly */
socket->context = context;
socket->timeout = 255;
socket->long_timeout = 255;
socket->low_prio_state = 0;
socket->connect_state = NULL;
us_internal_socket_context_link_socket(context, socket);

return socket;
}

struct us_connecting_socket_t *us_socket_context_connect(int ssl, struct us_socket_context_t *context, const char *host, int port, int options, int socket_ext_size, int* is_connecting) {
#ifndef LIBUS_NO_SSL
if (ssl) {
return us_internal_ssl_socket_context_connect((struct us_internal_ssl_socket_context_t *) context, host, port, options, socket_ext_size);
if (ssl == 1) {
return us_internal_ssl_socket_context_connect((struct us_internal_ssl_socket_context_t *) context, host, port, options, socket_ext_size, is_connecting);
}
#endif

struct us_loop_t* loop = us_socket_context_loop(ssl, context);

void* ptr;
if (Bun__addrinfo_get(loop, host, port, &ptr) == 0) {
// Fast-path: it's already cached.
// Avoid the connection logic.
*is_connecting = 1;
return (struct us_connecting_socket_t *) us_socket_context_connect_resolved_dns(context, ptr, options, socket_ext_size);
}

struct us_connecting_socket_t *c = us_calloc(1, sizeof(struct us_connecting_socket_t) + socket_ext_size);
c->socket_ext_size = socket_ext_size;
c->context = context;
Expand All @@ -360,14 +407,14 @@ struct us_connecting_socket_t *us_socket_context_connect(int ssl, struct us_sock
c->long_timeout = 255;
c->pending_resolve_callback = 1;

Bun__addrinfo_get(host, port, c);

#ifdef _WIN32
context->loop->uv_loop->active_handles++;
loop->uv_loop->active_handles++;
#else
context->loop->num_polls++;
loop->num_polls++;
#endif

Bun__addrinfo_set(ptr, c);

return c;
}

Expand Down Expand Up @@ -403,6 +450,7 @@ void us_internal_socket_after_resolve(struct us_connecting_socket_t *c) {
}

Bun__addrinfo_freeRequest(c->addrinfo_req, 0);
bsd_socket_nodelay(connect_socket_fd, 1);

struct us_socket_t *s = (struct us_socket_t *)us_create_poll(c->context->loop, 0, sizeof(struct us_socket_t) + c->socket_ext_size);
s->context = c->context;
Expand All @@ -414,13 +462,14 @@ void us_internal_socket_after_resolve(struct us_connecting_socket_t *c) {
// TODO check this, specifically how it interacts with the SSL code
memcpy(us_socket_ext(0, s), us_connecting_socket_ext(0, c), c->socket_ext_size);

// store the socket so we can close it if we need to
c->socket = s;
s->connect_state = c;

/* Connect sockets are semi-sockets just like listen sockets */
us_poll_init(&s->p, connect_socket_fd, POLL_TYPE_SEMI_SOCKET);
us_poll_start(&s->p, s->context->loop, LIBUS_SOCKET_WRITABLE);

// store the socket so we can close it if we need to
c->socket = s;
s->connect_state = c;
}

struct us_socket_t *us_socket_context_connect_unix(int ssl, struct us_socket_context_t *context, const char *server_path, size_t pathlen, int options, int socket_ext_size) {
Expand Down Expand Up @@ -599,6 +648,17 @@ void us_socket_context_on_connect_error(int ssl, struct us_socket_context_t *con
context->on_connect_error = on_connect_error;
}

void us_socket_context_on_socket_connect_error(int ssl, struct us_socket_context_t *context, struct us_socket_t *(*on_connect_error)(struct us_socket_t *s, int code)) {
#ifndef LIBUS_NO_SSL
if (ssl) {
us_internal_ssl_socket_context_on_socket_connect_error((struct us_internal_ssl_socket_context_t *) context, (struct us_internal_ssl_socket_t * (*)(struct us_internal_ssl_socket_t *, int)) on_connect_error);
return;
}
#endif

context->on_socket_connect_error = on_connect_error;
}

void *us_socket_context_ext(int ssl, struct us_socket_context_t *context) {
#ifndef LIBUS_NO_SSL
if (ssl) {
Expand Down
Loading

0 comments on commit b15d47d

Please sign in to comment.