feat: rate limit error chaching (#2448)

* feat: rate limit error chaching

Rate limit error caching to alleviate PATs.

* refactor: improve code comments
This commit is contained in:
Rick Staa 2023-09-17 15:45:17 +02:00 committed by GitHub
parent 64f56e88b4
commit bc8eaecaf4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 54 additions and 18 deletions

View file

@ -77,7 +77,12 @@ export default async (req, res) => {
}), }),
); );
} catch (err) { } catch (err) {
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses. res.setHeader(
"Cache-Control",
`max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
CONSTANTS.ERROR_CACHE_SECONDS
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
); // Use lower cache period for errors.
return res.send(renderError(err.message, err.secondaryMessage)); return res.send(renderError(err.message, err.secondaryMessage));
} }
}; };

View file

@ -57,7 +57,7 @@ export default async (req, res) => {
); );
let cacheSeconds = clampValue( let cacheSeconds = clampValue(
parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10), parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10),
CONSTANTS.FOUR_HOURS, CONSTANTS.FOUR_HOURS,
CONSTANTS.ONE_DAY, CONSTANTS.ONE_DAY,
); );
@ -100,7 +100,12 @@ export default async (req, res) => {
}), }),
); );
} catch (err) { } catch (err) {
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses. res.setHeader(
"Cache-Control",
`max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
CONSTANTS.ERROR_CACHE_SECONDS
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
); // Use lower cache period for errors.
return res.send(renderError(err.message, err.secondaryMessage)); return res.send(renderError(err.message, err.secondaryMessage));
} }
}; };

View file

@ -40,7 +40,7 @@ export default async (req, res) => {
const repoData = await fetchRepo(username, repo); const repoData = await fetchRepo(username, repo);
let cacheSeconds = clampValue( let cacheSeconds = clampValue(
parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10), parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10),
CONSTANTS.FOUR_HOURS, CONSTANTS.FOUR_HOURS,
CONSTANTS.ONE_DAY, CONSTANTS.ONE_DAY,
); );
@ -83,7 +83,12 @@ export default async (req, res) => {
}), }),
); );
} catch (err) { } catch (err) {
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses. res.setHeader(
"Cache-Control",
`max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
CONSTANTS.ERROR_CACHE_SECONDS
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
); // Use lower cache period for errors.
return res.send(renderError(err.message, err.secondaryMessage)); return res.send(renderError(err.message, err.secondaryMessage));
} }
}; };

View file

@ -63,7 +63,7 @@ export default async (req, res) => {
); );
let cacheSeconds = clampValue( let cacheSeconds = clampValue(
parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10), parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10),
CONSTANTS.FOUR_HOURS, CONSTANTS.FOUR_HOURS,
CONSTANTS.ONE_DAY, CONSTANTS.ONE_DAY,
); );
@ -99,7 +99,12 @@ export default async (req, res) => {
}), }),
); );
} catch (err) { } catch (err) {
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses. res.setHeader(
"Cache-Control",
`max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
CONSTANTS.ERROR_CACHE_SECONDS
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
); // Use lower cache period for errors.
return res.send(renderError(err.message, err.secondaryMessage)); return res.send(renderError(err.message, err.secondaryMessage));
} }
}; };

View file

@ -42,7 +42,7 @@ export default async (req, res) => {
const stats = await fetchWakatimeStats({ username, api_domain }); const stats = await fetchWakatimeStats({ username, api_domain });
let cacheSeconds = clampValue( let cacheSeconds = clampValue(
parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10), parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10),
CONSTANTS.FOUR_HOURS, CONSTANTS.FOUR_HOURS,
CONSTANTS.ONE_DAY, CONSTANTS.ONE_DAY,
); );
@ -50,10 +50,6 @@ export default async (req, res) => {
? parseInt(process.env.CACHE_SECONDS, 10) || cacheSeconds ? parseInt(process.env.CACHE_SECONDS, 10) || cacheSeconds
: cacheSeconds; : cacheSeconds;
if (!cache_seconds) {
cacheSeconds = CONSTANTS.FOUR_HOURS;
}
res.setHeader( res.setHeader(
"Cache-Control", "Cache-Control",
`max-age=${ `max-age=${
@ -82,7 +78,12 @@ export default async (req, res) => {
}), }),
); );
} catch (err) { } catch (err) {
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses. res.setHeader(
"Cache-Control",
`max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
CONSTANTS.ERROR_CACHE_SECONDS
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
); // Use lower cache period for errors.
return res.send(renderError(err.message, err.secondaryMessage)); return res.send(renderError(err.message, err.secondaryMessage));
} }
}; };

View file

@ -72,5 +72,5 @@ const retryer = async (fetcher, variables, retries = 0) => {
} }
}; };
export { retryer }; export { retryer, RETRIES };
export default retryer; export default retryer;

View file

@ -373,11 +373,21 @@ const noop = () => {};
const logger = const logger =
process.env.NODE_ENV !== "test" ? console : { log: noop, error: noop }; process.env.NODE_ENV !== "test" ? console : { log: noop, error: noop };
// Cache settings.
const CARD_CACHE_SECONDS = 14400;
const ERROR_CACHE_SECONDS = 600;
const CONSTANTS = { const CONSTANTS = {
ONE_MINUTE: 60,
FIVE_MINUTES: 300,
TEN_MINUTES: 600,
FIFTEEN_MINUTES: 900,
THIRTY_MINUTES: 1800, THIRTY_MINUTES: 1800,
TWO_HOURS: 7200, TWO_HOURS: 7200,
FOUR_HOURS: 14400, FOUR_HOURS: 14400,
ONE_DAY: 86400, ONE_DAY: 86400,
CARD_CACHE_SECONDS,
ERROR_CACHE_SECONDS,
}; };
const SECONDARY_ERROR_MESSAGES = { const SECONDARY_ERROR_MESSAGES = {

View file

@ -184,13 +184,18 @@ describe("Test /api/", () => {
]); ]);
}); });
it("should not store cache when error", async () => { it("should set shorter cache when error", async () => {
const { req, res } = faker({}, error); const { req, res } = faker({}, error);
await api(req, res); await api(req, res);
expect(res.setHeader.mock.calls).toEqual([ expect(res.setHeader.mock.calls).toEqual([
["Content-Type", "image/svg+xml"], ["Content-Type", "image/svg+xml"],
["Cache-Control", `no-cache, no-store, must-revalidate`], [
"Cache-Control",
`max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
CONSTANTS.ERROR_CACHE_SECONDS
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
],
]); ]);
}); });

View file

@ -1,6 +1,6 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { retryer } from "../src/common/retryer.js"; import { retryer, RETRIES } from "../src/common/retryer.js";
import { logger } from "../src/common/utils.js"; import { logger } from "../src/common/utils.js";
import { expect, it, describe } from "@jest/globals"; import { expect, it, describe } from "@jest/globals";
@ -44,7 +44,7 @@ describe("Test Retryer", () => {
try { try {
await retryer(fetcherFail, {}); await retryer(fetcherFail, {});
} catch (err) { } catch (err) {
expect(fetcherFail).toBeCalledTimes(8); expect(fetcherFail).toBeCalledTimes(RETRIES + 1);
expect(err.message).toBe("Downtime due to GitHub API rate limiting"); expect(err.message).toBe("Downtime due to GitHub API rate limiting");
} }
}); });