From d114821fb5292bfa605999196057acf98f8d8612 Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Tue, 10 Sep 2024 07:55:05 -0600 Subject: [PATCH] feat(arns): add a promise cache for arns middleware This protects against multiple calls to `nameResolver.resolver(name)` while resolving a single name. The cache is given a short TTL and requests are removed after they are resolved. The best way to verify this behavior is trigger concurrent requests to resolve an arns name (e.g. using `hey`) and observing the logs to resolve the name only appear once for all the requests. Once the original promise is resolved, every outstanding request is resolved with the result and the name & promise are removed from the request cache. --- src/middleware/arns.ts | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/middleware/arns.ts b/src/middleware/arns.ts index 2470ac7e..bf4d0949 100644 --- a/src/middleware/arns.ts +++ b/src/middleware/arns.ts @@ -22,10 +22,19 @@ import * as config from '../config.js'; import { headerNames } from '../constants.js'; import { sendNotFound } from '../routes/data/handlers.js'; import { DATA_PATH_REGEX } from '../constants.js'; -import { NameResolver } from '../types.js'; +import { NameResolution, NameResolver } from '../types.js'; import * as metrics from '../metrics.js'; +import NodeCache from 'node-cache'; + const EXCLUDED_SUBDOMAINS = new Set('www'); +// simple cache that stores the arns resolution promises to avoid duplicate requests to the name resolver +const arnsRequestCache = new NodeCache({ + stdTTL: 60, // short cache in case we forget to delete + checkperiod: 60, + useClones: false, // cloning promises is unsafe +}); + export const createArnsMiddleware = ({ dataHandler, nameResolver, @@ -67,9 +76,25 @@ export const createArnsMiddleware = ({ return; } + const getArnsResolutionPromise = async (): Promise => { + if (arnsRequestCache.has(arnsSubdomain)) { + const arnsResolutionPromise = + arnsRequestCache.get>(arnsSubdomain); + if (arnsResolutionPromise) { + return arnsResolutionPromise; + } + } + const arnsResolutionPromise = nameResolver.resolve(arnsSubdomain); + arnsRequestCache.set(arnsSubdomain, arnsResolutionPromise); + return arnsResolutionPromise; + }; + const start = Date.now(); const { resolvedId, ttl, processId } = - await nameResolver.resolve(arnsSubdomain); + await getArnsResolutionPromise().finally(() => { + // remove from cache after resolution + arnsRequestCache.del(arnsSubdomain); + }); metrics.arnsResolutionTime.observe(Date.now() - start); if (resolvedId === undefined) { sendNotFound(res);