Use a redirect for live outfits instead

I'm gonna try and see about getting Fastly to redirect these internally, so we can get all the benefits of CDN-caching the generated image, without forcing the user through another round-trip!
This commit is contained in:
Emi Matchu 2021-05-27 18:33:04 -07:00
parent d461686bc3
commit f932498066

View file

@ -38,6 +38,7 @@ import fetch from "node-fetch";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { print as graphqlPrint } from "graphql/language/printer"; import { print as graphqlPrint } from "graphql/language/printer";
import connectToDb from "../src/server/db";
import { renderOutfitImage } from "../src/server/outfit-images"; import { renderOutfitImage } from "../src/server/outfit-images";
import getVisibleLayers, { import getVisibleLayers, {
petAppearanceFragmentForGetVisibleLayers, petAppearanceFragmentForGetVisibleLayers,
@ -56,16 +57,9 @@ async function handle(req, res) {
} }
let layerUrls; let layerUrls;
let isSafeToCacheLongTerm;
if (req.query.layerUrls) { if (req.query.layerUrls) {
layerUrls = req.query.layerUrls.split(","); layerUrls = req.query.layerUrls.split(",");
} else if (req.query.id && req.query.updatedAt) {
// When layerUrls are provided, it's always safe to cache long-term. We
// assume layer assets are immutable, and that TNT generally creates new
// IDs when they're not. (Or, if TNT's conversion strategy or our rendering
// strategy dramatically changes, we might add a cache-buster to the URL.)
isSafeToCacheLongTerm = true;
} else if (req.query.id) {
const outfitId = req.query.id; const outfitId = req.query.id;
try { try {
layerUrls = await loadLayerUrlsForSavedOutfit(outfitId, size); layerUrls = await loadLayerUrlsForSavedOutfit(outfitId, size);
@ -77,10 +71,35 @@ async function handle(req, res) {
500 500
); );
} }
} else if (req.query.id) {
// If there's an outfit ID, but no `updatedAt`, redirect to the URL with
// `updatedAt` added. (NOTE: Our Fastly config will try to handle this
// redirect internally, instead of making the user do a round-trip! That
// way, we load the version cached at the CDN instead of regenerating it,
// if possible.)
const outfitId = req.query.id;
let updatedAt;
try {
updatedAt = await loadUpdatedAtForSavedOutfit(outfitId);
} catch (e) {
return reject(
res,
`Error loading data for outfit ${outfitId}: ${e.message}`,
500
);
}
// When an outfit ID is provided, it's only safe to cache long-term if const updatedAtTimestamp = Math.floor(updatedAt.getTime() / 1000);
// `updatedAt` is also provided. const urlWithUpdatedAt =
isSafeToCacheLongTerm = Boolean(req.query.updatedAt); `/outfits` +
`/${encodeURIComponent(outfitId)}` +
`/v/${encodeURIComponent(updatedAtTimestamp)}` +
`/${encodeURIComponent(req.query.size)}.png`;
// Cache this result for 10 minutes, so individual users don't wait on
// image reloads too much, but it's still always relatively fresh!
res.setHeader("Cache-Control", "public, max-age=600");
return res.redirect(urlWithUpdatedAt);
} else { } else {
return reject(res, `Missing required parameter: layerUrls`); return reject(res, `Missing required parameter: layerUrls`);
} }
@ -101,16 +120,11 @@ async function handle(req, res) {
const { image, status } = imageResult; const { image, status } = imageResult;
if (status === "success" && isSafeToCacheLongTerm) { if (status === "success") {
// This image is safe to cache long-term, so send a long-term cache header! // This image is ready, and it either used `layerUrls` or `updatedAt`, so
// it shouldn't change much, if ever. Send a long-term cache header!
res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
res.status(200); res.status(200);
} else if (status === "success") {
// This image rendered successfully, but isn't safe to cache long-term. We
// cache for a short period of time, instead, to avoid thrashing too hard
// on individual users, while still keeping it relatively fresh.
res.setHeader("Cache-Control", "public, max-age=600, immutable");
res.status(200);
} else { } else {
// On partial failure, we still send the image, but with a 500 status. We // On partial failure, we still send the image, but with a 500 status. We
// send a one-week cache header, but in such a way that the user can // send a one-week cache header, but in such a way that the user can
@ -186,6 +200,18 @@ async function loadLayerUrlsForSavedOutfit(outfitId, size) {
.map((layer) => layer.imageUrl); .map((layer) => layer.imageUrl);
} }
async function loadUpdatedAtForSavedOutfit(outfitId) {
const db = await connectToDb();
const [rows] = await db.query(`SELECT updated_at FROM outfits WHERE id = ?`, [
outfitId,
]);
const row = rows[0];
if (!row) {
throw new Error(`outfit ${outfitId} not found`);
}
return row.updated_at;
}
function reject(res, message, status = 400) { function reject(res, message, status = 400) {
res.setHeader("Content-Type", "text/plain"); res.setHeader("Content-Type", "text/plain");
return res.status(status).send(message); return res.status(status).send(message);