Skip to content

Commit

Permalink
On KV 404, re-insert with 60s TTL to avoid long 404 cache HITs (cloud…
Browse files Browse the repository at this point in the history
  • Loading branch information
WalshyDev authored Jan 15, 2025
1 parent 19228e5 commit 97603f0
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/lovely-years-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/workers-shared": patch
---

fix: on a 404 from KV, we do not want the asset to stay in cache for the normal 1 year TTL. Instead we want to re-insert with a 60s TTL to revalidate and prevent a bad 404 from persisting.
19 changes: 17 additions & 2 deletions packages/workers-shared/asset-worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
UnsafePerformanceTimer,
} from "../../utils/types";
import type { ColoMetadata, Environment, ReadyAnalytics } from "./types";
import type { Toucan } from "toucan-js";

export type Env = {
/*
Expand Down Expand Up @@ -100,7 +101,7 @@ export default class extends WorkerEntrypoint<Env> {
});
}

return await this.env.JAEGER.enterSpan("handleRequest", async (span) => {
return this.env.JAEGER.enterSpan("handleRequest", async (span) => {
span.setTags({
hostname: url.hostname,
eyeballPath: url.pathname,
Expand All @@ -114,8 +115,22 @@ export default class extends WorkerEntrypoint<Env> {
this.unstable_exists.bind(this),
this.unstable_getByETag.bind(this)
);
});
}).catch((err) =>
this.handleError(sentry, analytics, performance, startTimeMs, err)
);
} catch (err) {
return this.handleError(sentry, analytics, performance, startTimeMs, err);
}
}

handleError(
sentry: Toucan | undefined,
analytics: Analytics,
performance: PerformanceTimer,
startTimeMs: number,
err: unknown
) {
try {
const response = new InternalServerErrorResponse(err as Error);

// Log to Sentry if we can
Expand Down
34 changes: 30 additions & 4 deletions packages/workers-shared/asset-worker/src/utils/kv.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,46 @@
import type { Toucan } from "toucan-js";

export type AssetMetadata = {
contentType: string;
};

export async function getAssetWithMetadataFromKV(
assetsKVNamespace: KVNamespace,
assetKey: string,
sentry?: Toucan,
retries = 1
) {
let attempts = 0;

while (attempts <= retries) {
try {
return await assetsKVNamespace.getWithMetadata<AssetMetadata>(assetKey, {
type: "stream",
cacheTtl: 31536000, // 1 year
});
const asset = await assetsKVNamespace.getWithMetadata<AssetMetadata>(
assetKey,
{
type: "stream",
cacheTtl: 31536000, // 1 year
}
);

if (asset.value === null) {
// Don't cache a 404 for a year by re-requesting with a minimum cacheTtl
const retriedAsset =
await assetsKVNamespace.getWithMetadata<AssetMetadata>(assetKey, {
type: "stream",
cacheTtl: 60, // Minimum value allowed
});

if (retriedAsset.value !== null && sentry) {
sentry.captureException(
new Error(
`Initial request for asset ${assetKey} failed, but subsequent request succeeded.`
)
);
}

return retriedAsset;
}
return asset;
} catch (err) {
if (attempts >= retries) {
throw new Error(
Expand Down
29 changes: 28 additions & 1 deletion packages/workers-shared/asset-worker/tests/kv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,38 @@ describe("[Asset Worker] Fetching assets from KV", () => {
spy.mockReturnValue(Promise.reject("Oeps! Something went wrong"));

await expect(() =>
getAssetWithMetadataFromKV(mockKVNamespace, "abcd", 2)
getAssetWithMetadataFromKV(mockKVNamespace, "abcd", undefined, 2)
).rejects.toThrowError(
"Requested asset abcd could not be fetched from KV namespace."
);
expect(spy).toHaveBeenCalledTimes(3);
});

it("should retry on 404 and cache with 30s ttl", async () => {
let attempts = 0;
spy.mockImplementation(() => {
if (attempts++ === 0) {
return Promise.resolve({
value: null,
}) as unknown as Promise<
KVNamespaceGetWithMetadataResult<ReadableStream, AssetMetadata>
>;
} else {
return Promise.resolve({
value: "<html>Hello world</html>",
metadata: {
contentType: "text/html",
},
}) as unknown as Promise<
KVNamespaceGetWithMetadataResult<ReadableStream, AssetMetadata>
>;
}
});

const asset = await getAssetWithMetadataFromKV(mockKVNamespace, "abcd");
expect(asset?.value).toBeTruthy();
// Once for the initial call, once for the 404
expect(spy).toHaveBeenCalledTimes(2);
});
});
});

0 comments on commit 97603f0

Please sign in to comment.