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

Commit

Permalink
feat(dns): add option to enable app-level DNS caching
Browse files Browse the repository at this point in the history
  • Loading branch information
10xLaCroixDrinker committed Apr 29, 2022
1 parent 5ba844d commit 3f741e5
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 8 deletions.
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);
});
});
25 changes: 25 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,30 @@ 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 dedault max TTL is `Infinity`.

## `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 @@ -87,6 +87,7 @@
"@americanexpress/vitruvius": "^2.0.2",
"abort-controller": "^3.0.0",
"body-parser": "^1.19.0",
"cacheable-lookup": "^6.0.4",
"chalk": "^4.1.2",
"compression": "^1.7.4",
"cookie-parser": "^1.4.5",
Expand Down Expand Up @@ -156,6 +157,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)
) {
installCacheableLookup(maxTtl);
}
}

0 comments on commit 3f741e5

Please sign in to comment.