Skip to content

Commit 3797d25

Browse files
committed
perf: memoize blobs requests in the request scope
1 parent e1331ba commit 3797d25

File tree

2 files changed

+60
-18
lines changed

2 files changed

+60
-18
lines changed

src/run/handlers/cache.cts

+5-13
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,18 @@ import { getTracer } from './tracer.cjs'
3131

3232
type TagManifest = { revalidatedAt: number }
3333

34-
type TagManifestBlobCache = Record<string, Promise<TagManifest | null>>
35-
3634
const purgeCacheUserAgent = `${nextRuntimePkgName}@${nextRuntimePkgVersion}`
3735

3836
export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
3937
options: CacheHandlerContext
4038
revalidatedTags: string[]
4139
cacheStore: MemoizedKeyValueStoreBackedByRegionalBlobStore
4240
tracer = getTracer()
43-
tagManifestsFetchedFromBlobStoreInCurrentRequest: TagManifestBlobCache
4441

4542
constructor(options: CacheHandlerContext) {
4643
this.options = options
4744
this.revalidatedTags = options.revalidatedTags
4845
this.cacheStore = getMemoizedKeyValueStoreBackedByRegionalBlobStore({ consistency: 'strong' })
49-
this.tagManifestsFetchedFromBlobStoreInCurrentRequest = {}
5046
}
5147

5248
private getTTL(blob: NetlifyCacheHandlerValue) {
@@ -478,7 +474,7 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
478474
}
479475

480476
resetRequestCache() {
481-
this.tagManifestsFetchedFromBlobStoreInCurrentRequest = {}
477+
// no-op
482478
}
483479

484480
/**
@@ -530,14 +526,10 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
530526
const tagManifestPromises: Promise<boolean>[] = []
531527

532528
for (const tag of cacheTags) {
533-
let tagManifestPromise: Promise<TagManifest | null> =
534-
this.tagManifestsFetchedFromBlobStoreInCurrentRequest[tag]
535-
536-
if (!tagManifestPromise) {
537-
tagManifestPromise = this.cacheStore.get<TagManifest>(tag, 'tagManifest.get')
538-
539-
this.tagManifestsFetchedFromBlobStoreInCurrentRequest[tag] = tagManifestPromise
540-
}
529+
const tagManifestPromise: Promise<TagManifest | null> = this.cacheStore.get<TagManifest>(
530+
tag,
531+
'tagManifest.get',
532+
)
541533

542534
tagManifestPromises.push(
543535
tagManifestPromise.then((tagManifest) => {

src/run/regional-blob-store.cts

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

3+
import { getRequestContext, RequestContext } from './handlers/request-context.cjs'
34
import { getTracer } from './handlers/tracer.cjs'
45

56
const FETCH_BEFORE_NEXT_PATCHED_IT = Symbol.for('nf-not-patched-fetch')
@@ -68,6 +69,44 @@ const encodeBlobKey = async (key: string) => {
6869
return await encodeBlobKeyImpl(key)
6970
}
7071

72+
// hold in-memory cache tied to request in memory for as long as request context is alive
73+
const perRequestInMemoryStore = new WeakMap<
74+
RequestContext,
75+
Record<string, Promise<unknown> | unknown>
76+
>()
77+
78+
const getRequestSpecificInMemoryCache = () => {
79+
const requestContext = getRequestContext()
80+
if (!requestContext) {
81+
// Fallback to a no-op store if we can't find request context
82+
return {
83+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
84+
get<T>(key: string): Promise<T | null> | undefined {
85+
return undefined
86+
},
87+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
88+
set(key: string, value: unknown) {
89+
// no-op
90+
},
91+
}
92+
}
93+
94+
let perRequestStore = perRequestInMemoryStore.get(requestContext)
95+
if (!perRequestStore) {
96+
perRequestStore = {}
97+
perRequestInMemoryStore.set(requestContext, perRequestStore)
98+
}
99+
100+
return {
101+
get<T>(key: string): Promise<T | null> | undefined {
102+
return perRequestStore[key] as Promise<T | null> | undefined
103+
},
104+
set(key: string, value: unknown) {
105+
perRequestStore[key] = value
106+
},
107+
}
108+
}
109+
71110
export const getMemoizedKeyValueStoreBackedByRegionalBlobStore = (
72111
args: GetWithMetadataOptions = {},
73112
) => {
@@ -76,21 +115,32 @@ export const getMemoizedKeyValueStoreBackedByRegionalBlobStore = (
76115

77116
return {
78117
async get<T>(key: string, otelSpanTitle: string): Promise<T | null> {
79-
const blobKey = await encodeBlobKey(key)
118+
const inMemoryCache = getRequestSpecificInMemoryCache()
80119

81-
return tracer.withActiveSpan(otelSpanTitle, async (span) => {
120+
const memoizedValue = inMemoryCache.get<T>(key)
121+
if (memoizedValue) {
122+
return memoizedValue
123+
}
124+
125+
const blobKey = await encodeBlobKey(key)
126+
const getPromise = tracer.withActiveSpan(otelSpanTitle, async (span) => {
82127
span.setAttributes({ key, blobKey })
83-
const blob = await store.get(key, { type: 'json' })
128+
const blob = await store.get(blobKey, { type: 'json' })
84129
span.addEvent(blob ? 'Hit' : 'Miss')
85130
return blob
86131
})
132+
inMemoryCache.set(key, getPromise)
133+
return getPromise
87134
},
88135
async set(key: string, value: unknown, otelSpanTitle: string) {
89-
const blobKey = await encodeBlobKey(key)
136+
const inMemoryCache = getRequestSpecificInMemoryCache()
90137

138+
inMemoryCache.set(key, value)
139+
140+
const blobKey = await encodeBlobKey(key)
91141
return tracer.withActiveSpan(otelSpanTitle, async (span) => {
92142
span.setAttributes({ key, blobKey })
93-
return await store.setJSON(key, value)
143+
return await store.setJSON(blobKey, value)
94144
})
95145
},
96146
}

0 commit comments

Comments
 (0)