Skip to content

Commit 2e7a951

Browse files
committed
refactor: create simplified key-value store interface to interact with blobs with common tracing pattern
1 parent 03f168d commit 2e7a951

File tree

7 files changed

+89
-74
lines changed

7 files changed

+89
-74
lines changed

src/build/content/static.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { trace } from '@opentelemetry/api'
66
import { wrapTracer } from '@opentelemetry/api/experimental'
77
import glob from 'fast-glob'
88

9-
import type { HtmlBlob } from '../../run/next.cjs'
109
import { encodeBlobKey } from '../../shared/blobkey.js'
10+
import type { HtmlBlob } from '../../shared/cache-types.cjs'
1111
import { PluginContext } from '../plugin-context.js'
1212
import { verifyNetlifyForms } from '../verification.js'
1313

src/run/handlers/cache.cts

+21-38
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Buffer } from 'node:buffer'
55
import { join } from 'node:path'
66
import { join as posixJoin } from 'node:path/posix'
77

8-
import { Store } from '@netlify/blobs'
98
import { purgeCache } from '@netlify/functions'
109
import { type Span } from '@opentelemetry/api'
1110
import type { PrerenderManifest } from 'next/dist/build/index.js'
@@ -21,37 +20,34 @@ import {
2120
type NetlifyCachedRouteValue,
2221
type NetlifyCacheHandlerValue,
2322
type NetlifyIncrementalCacheValue,
23+
type TagManifest,
2424
} from '../../shared/cache-types.cjs'
25-
import { getRegionalBlobStore } from '../regional-blob-store.cjs'
25+
import {
26+
getMemoizedKeyValueStoreBackedByRegionalBlobStore,
27+
MemoizedKeyValueStoreBackedByRegionalBlobStore,
28+
} from '../regional-blob-store.cjs'
2629

2730
import { getLogger, getRequestContext } from './request-context.cjs'
2831
import { getTracer } from './tracer.cjs'
2932

30-
type TagManifest = { revalidatedAt: number }
31-
32-
type TagManifestBlobCache = Record<string, Promise<TagManifest>>
33+
type TagManifestBlobCache = Record<string, Promise<TagManifest | null>>
3334

3435
const purgeCacheUserAgent = `${nextRuntimePkgName}@${nextRuntimePkgVersion}`
3536

3637
export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
3738
options: CacheHandlerContext
3839
revalidatedTags: string[]
39-
blobStore: Store
40+
cacheStore: MemoizedKeyValueStoreBackedByRegionalBlobStore
4041
tracer = getTracer()
4142
tagManifestsFetchedFromBlobStoreInCurrentRequest: TagManifestBlobCache
4243

4344
constructor(options: CacheHandlerContext) {
4445
this.options = options
4546
this.revalidatedTags = options.revalidatedTags
46-
this.blobStore = getRegionalBlobStore({ consistency: 'strong' })
47+
this.cacheStore = getMemoizedKeyValueStoreBackedByRegionalBlobStore({ consistency: 'strong' })
4748
this.tagManifestsFetchedFromBlobStoreInCurrentRequest = {}
4849
}
4950

50-
private async encodeBlobKey(key: string) {
51-
const { encodeBlobKey } = await import('../../shared/blobkey.js')
52-
return await encodeBlobKey(key)
53-
}
54-
5551
private getTTL(blob: NetlifyCacheHandlerValue) {
5652
if (
5753
blob.value?.kind === 'FETCH' ||
@@ -245,19 +241,13 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
245241
const [key, ctx = {}] = args
246242
getLogger().debug(`[NetlifyCacheHandler.get]: ${key}`)
247243

248-
const blobKey = await this.encodeBlobKey(key)
249-
span.setAttributes({ key, blobKey })
244+
span.setAttributes({ key })
250245

251-
const blob = (await this.tracer.withActiveSpan('blobStore.get', async (blobGetSpan) => {
252-
blobGetSpan.setAttributes({ key, blobKey })
253-
return await this.blobStore.get(blobKey, {
254-
type: 'json',
255-
})
256-
})) as NetlifyCacheHandlerValue | null
246+
const blob = await this.cacheStore.get<NetlifyCacheHandlerValue>(key, 'blobStore.get')
257247

258248
// if blob is null then we don't have a cache entry
259249
if (!blob) {
260-
span.addEvent('Cache miss', { key, blobKey })
250+
span.addEvent('Cache miss', { key })
261251
return null
262252
}
263253

@@ -268,7 +258,6 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
268258
// but opt to discard STALE data, so that Next.js generate fresh response
269259
span.addEvent('Discarding stale entry due to SWR background revalidation request', {
270260
key,
271-
blobKey,
272261
ttl,
273262
})
274263
getLogger()
@@ -285,7 +274,7 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
285274
const staleByTags = await this.checkCacheEntryStaleByTags(blob, ctx.tags, ctx.softTags)
286275

287276
if (staleByTags) {
288-
span.addEvent('Stale', { staleByTags, key, blobKey, ttl })
277+
span.addEvent('Stale', { staleByTags, key, ttl })
289278
return null
290279
}
291280

@@ -403,9 +392,8 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
403392
async set(...args: Parameters<CacheHandlerForMultipleVersions['set']>) {
404393
return this.tracer.withActiveSpan('set cache key', async (span) => {
405394
const [key, data, context] = args
406-
const blobKey = await this.encodeBlobKey(key)
407395
const lastModified = Date.now()
408-
span.setAttributes({ key, lastModified, blobKey })
396+
span.setAttributes({ key, lastModified })
409397

410398
getLogger().debug(`[NetlifyCacheHandler.set]: ${key}`)
411399

@@ -415,10 +403,7 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
415403
// and we didn't yet capture cache tags, we try to get cache tags from freshly produced cache value
416404
this.captureCacheTags(value, key)
417405

418-
await this.blobStore.setJSON(blobKey, {
419-
lastModified,
420-
value,
421-
})
406+
await this.cacheStore.set(key, { lastModified, value }, 'blobStore.set')
422407

423408
if (data?.kind === 'PAGE' || data?.kind === 'PAGES') {
424409
const requestContext = getRequestContext()
@@ -476,7 +461,7 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
476461
await Promise.all(
477462
tags.map(async (tag) => {
478463
try {
479-
await this.blobStore.setJSON(await this.encodeBlobKey(tag), data)
464+
await this.cacheStore.set(tag, data, 'tagManifest.set')
480465
} catch (error) {
481466
getLogger().withError(error).log(`Failed to update tag manifest for ${tag}`)
482467
}
@@ -544,23 +529,21 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
544529
const tagManifestPromises: Promise<boolean>[] = []
545530

546531
for (const tag of cacheTags) {
547-
let tagManifestPromise: Promise<TagManifest> =
532+
let tagManifestPromise: Promise<TagManifest | null> =
548533
this.tagManifestsFetchedFromBlobStoreInCurrentRequest[tag]
549534

550535
if (!tagManifestPromise) {
551-
tagManifestPromise = this.encodeBlobKey(tag).then((blobKey) => {
552-
return this.tracer.withActiveSpan(`get tag manifest`, async (span) => {
553-
span.setAttributes({ tag, blobKey })
554-
return this.blobStore.get(blobKey, { type: 'json' })
555-
})
556-
})
536+
tagManifestPromise = this.cacheStore.get<TagManifest>(tag, 'tagManifest.get')
557537

558538
this.tagManifestsFetchedFromBlobStoreInCurrentRequest[tag] = tagManifestPromise
559539
}
560540

561541
tagManifestPromises.push(
562542
tagManifestPromise.then((tagManifest) => {
563-
const isStale = tagManifest?.revalidatedAt >= (cacheEntry.lastModified || Date.now())
543+
if (!tagManifest) {
544+
return false
545+
}
546+
const isStale = tagManifest.revalidatedAt >= (cacheEntry.lastModified || Date.now())
564547
if (isStale) {
565548
resolve(true)
566549
return true

src/run/handlers/server.ts

-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ export default async (
127127
headers: response.headers,
128128
request,
129129
span,
130-
tracer,
131130
requestContext,
132131
})
133132
}

src/run/headers.ts

+6-21
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import type { Span } from '@opentelemetry/api'
22
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
33

4-
import { encodeBlobKey } from '../shared/blobkey.js'
5-
import type { NetlifyCachedRouteValue } from '../shared/cache-types.cjs'
4+
import type { NetlifyCachedRouteValue, NetlifyCacheHandlerValue } from '../shared/cache-types.cjs'
65

76
import { getLogger, RequestContext } from './handlers/request-context.cjs'
8-
import type { RuntimeTracer } from './handlers/tracer.cjs'
9-
import { getRegionalBlobStore } from './regional-blob-store.cjs'
7+
import { getMemoizedKeyValueStoreBackedByRegionalBlobStore } from './regional-blob-store.cjs'
108

119
const ALL_VARIATIONS = Symbol.for('ALL_VARIATIONS')
1210
interface NetlifyVaryValues {
@@ -129,13 +127,11 @@ export const adjustDateHeader = async ({
129127
headers,
130128
request,
131129
span,
132-
tracer,
133130
requestContext,
134131
}: {
135132
headers: Headers
136133
request: Request
137134
span: Span
138-
tracer: RuntimeTracer
139135
requestContext: RequestContext
140136
}) => {
141137
const key = new URL(request.url).pathname
@@ -157,23 +153,12 @@ export const adjustDateHeader = async ({
157153
warning: true,
158154
})
159155

160-
const blobStore = getRegionalBlobStore({ consistency: 'strong' })
161-
const blobKey = await encodeBlobKey(key)
162-
163-
// TODO: use metadata for this
164-
lastModified = await tracer.withActiveSpan(
156+
const cacheStore = getMemoizedKeyValueStoreBackedByRegionalBlobStore({ consistency: 'strong' })
157+
const cacheEntry = await cacheStore.get<NetlifyCacheHandlerValue>(
158+
key,
165159
'get cache to calculate date header',
166-
async (getBlobForDateSpan) => {
167-
getBlobForDateSpan.setAttributes({
168-
key,
169-
blobKey,
170-
})
171-
const blob = (await blobStore.get(blobKey, { type: 'json' })) ?? {}
172-
173-
getBlobForDateSpan.addEvent(blob ? 'Cache hit' : 'Cache miss')
174-
return blob.lastModified
175-
},
176160
)
161+
lastModified = cacheEntry?.lastModified
177162
}
178163

179164
if (!lastModified) {

src/run/next.cts

+5-12
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { relative, resolve } from 'path'
44
// @ts-expect-error no types installed
55
import { patchFs } from 'fs-monkey'
66

7+
import { HtmlBlob } from '../shared/cache-types.cjs'
8+
79
import { getRequestContext } from './handlers/request-context.cjs'
810
import { getTracer } from './handlers/tracer.cjs'
9-
import { getRegionalBlobStore } from './regional-blob-store.cjs'
11+
import { getMemoizedKeyValueStoreBackedByRegionalBlobStore } from './regional-blob-store.cjs'
1012

1113
// https://github.com/vercel/next.js/pull/68193/files#diff-37243d614f1f5d3f7ea50bbf2af263f6b1a9a4f70e84427977781e07b02f57f1R49
1214
// This import resulted in importing unbundled React which depending if NODE_ENV is `production` or not would use
@@ -76,18 +78,11 @@ ResponseCache.prototype.get = function get(...getArgs: unknown[]) {
7678

7779
type FS = typeof import('fs')
7880

79-
export type HtmlBlob = {
80-
html: string
81-
isFallback: boolean
82-
}
83-
8481
export async function getMockedRequestHandler(...args: Parameters<typeof getRequestHandlers>) {
8582
const tracer = getTracer()
8683
return tracer.withActiveSpan('mocked request handler', async () => {
8784
const ofs = { ...fs }
8885

89-
const { encodeBlobKey } = await import('../shared/blobkey.js')
90-
9186
async function readFileFallbackBlobStore(...fsargs: Parameters<FS['promises']['readFile']>) {
9287
const [path, options] = fsargs
9388
try {
@@ -97,11 +92,9 @@ export async function getMockedRequestHandler(...args: Parameters<typeof getRequ
9792
} catch (error) {
9893
// only try to get .html files from the blob store
9994
if (typeof path === 'string' && path.endsWith('.html')) {
100-
const store = getRegionalBlobStore()
95+
const cacheStore = getMemoizedKeyValueStoreBackedByRegionalBlobStore()
10196
const relPath = relative(resolve('.next/server/pages'), path)
102-
const file = (await store.get(await encodeBlobKey(relPath), {
103-
type: 'json',
104-
})) as HtmlBlob | null
97+
const file = await cacheStore.get<HtmlBlob>(relPath, 'staticHtml.get')
10598
if (file !== null) {
10699
if (!file.isFallback) {
107100
const requestContext = getRequestContext()

src/run/regional-blob-store.cts

+47-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { getDeployStore, GetWithMetadataOptions, Store } from '@netlify/blobs'
22

3+
import type { BlobType } from '../shared/cache-types.cjs'
4+
5+
import { getTracer } from './handlers/tracer.cjs'
6+
37
const FETCH_BEFORE_NEXT_PATCHED_IT = Symbol.for('nf-not-patched-fetch')
48
const extendedGlobalThis = globalThis as typeof globalThis & {
59
[FETCH_BEFORE_NEXT_PATCHED_IT]?: typeof globalThis.fetch
@@ -53,10 +57,52 @@ const fetchBeforeNextPatchedItFallback = forceOptOutOfUsingDataCache(
5357
const getFetchBeforeNextPatchedIt = () =>
5458
extendedGlobalThis[FETCH_BEFORE_NEXT_PATCHED_IT] ?? fetchBeforeNextPatchedItFallback
5559

56-
export const getRegionalBlobStore = (args: GetWithMetadataOptions = {}): Store => {
60+
const getRegionalBlobStore = (args: GetWithMetadataOptions = {}): Store => {
5761
return getDeployStore({
5862
...args,
5963
fetch: getFetchBeforeNextPatchedIt(),
6064
region: process.env.USE_REGIONAL_BLOBS?.toUpperCase() === 'TRUE' ? undefined : 'us-east-2',
6165
})
6266
}
67+
68+
const encodeBlobKey = async (key: string) => {
69+
const { encodeBlobKey: encodeBlobKeyImpl } = await import('../shared/blobkey.js')
70+
return await encodeBlobKeyImpl(key)
71+
}
72+
73+
export const getMemoizedKeyValueStoreBackedByRegionalBlobStore = (
74+
args: GetWithMetadataOptions = {},
75+
) => {
76+
const store = getRegionalBlobStore(args)
77+
const tracer = getTracer()
78+
79+
return {
80+
async get<T extends BlobType>(key: string, otelSpanTitle: string): Promise<T | null> {
81+
const blobKey = await encodeBlobKey(key)
82+
83+
return tracer.withActiveSpan(otelSpanTitle, async (span) => {
84+
span.setAttributes({ key, blobKey })
85+
const blob = (await store.get(blobKey, { type: 'json' })) as T | null
86+
span.addEvent(blob ? 'Hit' : 'Miss')
87+
return blob
88+
})
89+
},
90+
async set(key: string, value: BlobType, otelSpanTitle: string) {
91+
const blobKey = await encodeBlobKey(key)
92+
93+
return tracer.withActiveSpan(otelSpanTitle, async (span) => {
94+
span.setAttributes({ key, blobKey })
95+
return await store.setJSON(blobKey, value)
96+
})
97+
},
98+
}
99+
}
100+
101+
/**
102+
* Wrapper around Blobs Store that memoizes the cache entries within context of a request
103+
* to avoid duplicate requests to the same key and also allowing to read its own writes from
104+
* memory.
105+
*/
106+
export type MemoizedKeyValueStoreBackedByRegionalBlobStore = ReturnType<
107+
typeof getMemoizedKeyValueStoreBackedByRegionalBlobStore
108+
>

src/shared/cache-types.cts

+9
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,12 @@ export type CacheHandlerForMultipleVersions = BaseCacheHandlerForMultipleVersion
154154
context: CacheHandlerSetContextForMultipleVersions,
155155
) => ReturnType<BaseCacheHandlerForMultipleVersions['set']>
156156
}
157+
158+
export type TagManifest = { revalidatedAt: number }
159+
160+
export type HtmlBlob = {
161+
html: string
162+
isFallback: boolean
163+
}
164+
165+
export type BlobType = NetlifyCacheHandlerValue | TagManifest | HtmlBlob

0 commit comments

Comments
 (0)