Skip to content
This repository has been archived by the owner on May 3, 2024. It is now read-only.

feat(dns): add option to enable app-level DNS caching #727

Merged
merged 5 commits into from
May 4, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions __tests__/server/utils/onModuleLoad.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { extendRestrictedAttributesAllowList, validateSafeRequestRestrictedAttri
import { setConfigureRequestLog } from '../../../src/server/utils/logging/serverMiddleware';
import { setCreateSsrFetch } from '../../../src/server/utils/createSsrFetch';
import { getEventLoopDelayThreshold } from '../../../src/server/utils/createCircuitBreaker';
import setupDnsCache from '../../../src/server/utils/setupDnsCache';
import { configurePWA } from '../../../src/server/middleware/pwa';
import { setErrorPage } from '../../../src/server/middleware/sendHtml';

Expand All @@ -46,6 +47,7 @@ jest.mock('../../../src/server/middleware/conditionallyAllowCors', () => ({
}));
jest.mock('../../../src/server/utils/logging/serverMiddleware');
jest.mock('../../../src/server/utils/createSsrFetch');
jest.mock('../../../src/server/utils/setupDnsCache');

jest.mock('../../../src/server/utils/safeRequest', () => ({
extendRestrictedAttributesAllowList: jest.fn(),
Expand Down Expand Up @@ -347,6 +349,16 @@ describe('onModuleLoad', () => {
expect(configurePWA).toHaveBeenCalledWith(pwa);
});

it('calls setupDnsCache with DNS cache config', () => {
const dnsCache = { enabled: true, maxTtl: 300 };
onModuleLoad({
module: { [CONFIGURATION_KEY]: { csp, dnsCache }, [META_DATA_KEY]: { version: '1.0.16' } },
moduleName: 'some-root',
});
expect(setupDnsCache).toHaveBeenCalledTimes(1);
expect(setupDnsCache).toHaveBeenCalledWith(dnsCache);
});

it('calls setErrorPage with error page URL', () => {
const errorPageUrl = 'https://example.com';
onModuleLoad({
Expand Down
97 changes: 97 additions & 0 deletions __tests__/server/utils/setupDnsCache.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import http from 'http';
import https from 'https';
import CacheableLookup from 'cacheable-lookup';
import matchers from 'expect/build/matchers';
import setupDnsCache, {
uninstallCacheableLookup,
installCacheableLookup,
} from '../../../src/server/utils/setupDnsCache';

// Create a fake agent so we can get the Symbols used by cacheable-lookup
const fakeAgent = { createConnection: () => {} };
const cacheableInstance = new CacheableLookup();
cacheableInstance.install(fakeAgent);
const cacheableLookupSymbols = Object.getOwnPropertySymbols(fakeAgent);
// eslint-disable-next-line jest/no-standalone-expect -- validate we have the right symbols
expect(cacheableLookupSymbols).toMatchInlineSnapshot(`
Array [
Symbol(cacheableLookupCreateConnection),
Symbol(cacheableLookupInstance),
]
`);
const cacheableInstanceSymbol = cacheableLookupSymbols[1];
const getCacheableInstance = (protocol) => protocol.globalAgent[cacheableInstanceSymbol];

expect.extend({
toHaveCacheableLookupInstalled(input) {
return matchers.toEqual.call(
this,
Object.getOwnPropertySymbols(input.globalAgent),
expect.arrayContaining(cacheableLookupSymbols)
);
},
toHaveMaxTtl(input, expected) {
return matchers.toBe.call(
this,
getCacheableInstance(input).maxTtl,
expected
);
},
});

describe('setupDnsCache', () => {
beforeEach(uninstallCacheableLookup);

it('should uninstall cacheable lookup when DNS cache is not enabled and cacheable lookup is installed', () => {
installCacheableLookup();
expect(http).toHaveCacheableLookupInstalled();
expect(https).toHaveCacheableLookupInstalled();
setupDnsCache();
expect(http).not.toHaveCacheableLookupInstalled();
expect(https).not.toHaveCacheableLookupInstalled();
});

it('should not attempt to uninstall cacheable lookup if it is not installed', () => {
uninstallCacheableLookup();
expect(http).not.toHaveCacheableLookupInstalled();
expect(https).not.toHaveCacheableLookupInstalled();
setupDnsCache();
expect(http).not.toHaveCacheableLookupInstalled();
expect(https).not.toHaveCacheableLookupInstalled();
});

it('should install cacheable lookup when DNS cache is enabled and it is not installed', () => {
uninstallCacheableLookup();
expect(http).not.toHaveCacheableLookupInstalled();
expect(https).not.toHaveCacheableLookupInstalled();
setupDnsCache({ enabled: true });
expect(http).toHaveCacheableLookupInstalled();
expect(https).toHaveCacheableLookupInstalled();
});

it('should uninstall and reinstall cacheable lookup when the max TTL changes', () => {
installCacheableLookup(100);
const httpInstanceBefore = getCacheableInstance(http);
const httpsInstanceBefore = getCacheableInstance(https);
expect(http).toHaveMaxTtl(100);
expect(https).toHaveMaxTtl(100);
setupDnsCache({ enabled: true, maxTtl: 10 });
expect(http).toHaveMaxTtl(10);
expect(https).toHaveMaxTtl(10);
expect(getCacheableInstance(http)).not.toBe(httpInstanceBefore);
expect(getCacheableInstance(https)).not.toBe(httpsInstanceBefore);
});

it('should not reinstall cacheable lookup when DNS cache settings do not change', () => {
installCacheableLookup(100);
const httpInstanceBefore = getCacheableInstance(http);
const httpsInstanceBefore = getCacheableInstance(https);
expect(http).toHaveMaxTtl(100);
expect(https).toHaveMaxTtl(100);
setupDnsCache({ enabled: true, maxTtl: 100 });
expect(http).toHaveMaxTtl(100);
expect(https).toHaveMaxTtl(100);
expect(getCacheableInstance(http)).toBe(httpInstanceBefore);
expect(getCacheableInstance(https)).toBe(httpsInstanceBefore);
});
});
29 changes: 29 additions & 0 deletions docs/api/modules/App-Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ if (!global.BROWSER) {
createSsrFetch,
eventLoopDelayThreshold,
errorPageUrl,
dnsCache,
/* Child Module Specific */
validateStateConfig,
requiredSafeRequestRestrictedAttributes,
Expand Down Expand Up @@ -67,6 +68,7 @@ export default MyModule;
- [`createSsrFetch`](#createssrfetch)
- [`eventLoopDelayThreshold`](#eventloopdelaythreshold)
- [`errorPageUrl`](#errorpageurl)
- [`dnsCache`](#dnsCache)
- [`validateStateConfig`](#validatestateconfig)
- [`requiredSafeRequestRestrictedAttributes`](#requiredsaferequestrestrictedattributes)

Expand Down Expand Up @@ -477,7 +479,34 @@ if (!global.BROWSER) {

The `errorPageUrl` directive is useful for supplying a URL to a custom error page. This URL should return a `Content-Type` of `text/html` and a `Content-Length` of less than `244000`. The URL will get called when your root module is loaded and is rendered if and when an error occurs. It is recommended that you keep the custom error page as small as possible.

## `dnsCache`

**Module Type**

- ✅ Root Module
- 🚫 Child Module

**Shape**

```js
if (!global.BROWSER) {
Module.appConfig = {
dnsCache: {
enabled: Boolean,
maxTtl: Number,
},
};
}
```

The `dnsCache` option allows for enabling applcation-level DNS caching. It is disabled by default. When enabled, the default max TTL is `Infinity`.

`maxTtl` affects the maximum lifetime of the entries received from the specified DNS server (TTL in seconds). If set to 0, it will make a new DNS query each time. It is not recommended to be lower than the DNS server response time in order to prevent bottlenecks.



## `validateStateConfig`

**Module Type**
* 🚫 Root Module
* ✅ Child Module
Expand Down
33 changes: 25 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"abort-controller": "^3.0.0",
"body-parser": "^1.19.0",
"bytes": "^3.1.2",
"cacheable-lookup": "^6.0.4",
"chalk": "^4.1.2",
"compression": "^1.7.4",
"cookie-parser": "^1.4.5",
Expand Down Expand Up @@ -157,6 +158,7 @@
"eslint-plugin-es": "^4.1.0",
"eslint-plugin-jest": "^24.7.0",
"eslint-plugin-jest-dom": "^3.9.4",
"expect": "^27.5.1",
"find-up": "^5.0.0",
"fs-extra": "^9.0.1",
"glob": "^7.1.6",
Expand Down
3 changes: 3 additions & 0 deletions src/server/utils/onModuleLoad.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { extendRestrictedAttributesAllowList, validateSafeRequestRestrictedAttri
import { setConfigureRequestLog } from './logging/serverMiddleware';
import { setCreateSsrFetch } from './createSsrFetch';
import { setEventLoopDelayThreshold } from './createCircuitBreaker';
import setupDnsCache from './setupDnsCache';
import { configurePWA } from '../middleware/pwa';
import { validatePWAConfig } from './validation';
import { setErrorPage } from '../middleware/sendHtml';
Expand Down Expand Up @@ -87,6 +88,7 @@ export default function onModuleLoad({
eventLoopDelayThreshold,
pwa,
errorPageUrl,
dnsCache,
// Child Module Specific
requiredExternals,
validateStateConfig,
Expand Down Expand Up @@ -130,6 +132,7 @@ export default function onModuleLoad({
configurePWA(validatePWAConfig(pwa, {
clientStateConfig: getClientStateConfig(),
}));
setupDnsCache(dnsCache);

logModuleLoad(moduleName, metaData.version);
return;
Expand Down
31 changes: 31 additions & 0 deletions src/server/utils/setupDnsCache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import http from 'http';
import https from 'https';
import CacheableLookup from 'cacheable-lookup';

let cacheableInstance = null;

export function uninstallCacheableLookup() {
if (cacheableInstance !== null) {
cacheableInstance.uninstall(http.globalAgent);
cacheableInstance.uninstall(https.globalAgent);
cacheableInstance = null;
}
}

export function installCacheableLookup(maxTtl) {
uninstallCacheableLookup();
cacheableInstance = new CacheableLookup({ maxTtl });
cacheableInstance.install(http.globalAgent);
cacheableInstance.install(https.globalAgent);
}

export default function setupDnsCache({ enabled, maxTtl } = { enabled: false }) {
if (enabled !== true) {
uninstallCacheableLookup();
} else if (
enabled === true
&& (cacheableInstance == null || cacheableInstance.maxTtl !== maxTtl)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

) {
installCacheableLookup(maxTtl);
}
}