Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 2e7a951

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

File tree

Expand file treeCollapse file tree

7 files changed

+89
-74
lines changed
Filter options
Expand file treeCollapse file tree

7 files changed

+89
-74
lines changed

‎src/build/content/static.ts

Copy file name to clipboardExpand all lines: src/build/content/static.ts
+1-1Lines changed: 1 addition & 1 deletion
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

Copy file name to clipboardExpand all lines: src/run/handlers/cache.cts
+21-38Lines changed: 21 additions & 38 deletions
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

Copy file name to clipboardExpand all lines: src/run/handlers/server.ts
-1Lines changed: 0 additions & 1 deletion
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

Copy file name to clipboardExpand all lines: src/run/headers.ts
+6-21Lines changed: 6 additions & 21 deletions
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

Copy file name to clipboardExpand all lines: src/run/next.cts
+5-12Lines changed: 5 additions & 12 deletions
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

Copy file name to clipboardExpand all lines: src/run/regional-blob-store.cts
+47-1Lines changed: 47 additions & 1 deletion
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

Copy file name to clipboardExpand all lines: src/shared/cache-types.cts
+9Lines changed: 9 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.