-
Notifications
You must be signed in to change notification settings - Fork 87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: track revalidate / cdn purge to ensure it finishes execution and is not suspended mid-execution #2490
fix: track revalidate / cdn purge to ensure it finishes execution and is not suspended mid-execution #2490
Changes from 6 commits
c6a8699
bd9771b
55c7843
e88dd8c
b475810
5c1eddb
08f666d
dd43ce8
a72a0cd
17d9aec
2a054cc
1e2f02b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -283,19 +283,33 @@ export class NetlifyCacheHandler implements CacheHandler { | |
if (requestContext?.didPagesRouterOnDemandRevalidate) { | ||
const tag = `_N_T_${key === '/index' ? '/' : key}` | ||
getLogger().debug(`Purging CDN cache for: [${tag}]`) | ||
purgeCache({ tags: [tag] }).catch((error) => { | ||
// TODO: add reporting here | ||
getLogger() | ||
.withError(error) | ||
.error(`[NetlifyCacheHandler]: Purging the cache for tag ${tag} failed`) | ||
}) | ||
requestContext.trackBackgroundWork( | ||
purgeCache({ tags: [tag] }).catch((error) => { | ||
// TODO: add reporting here | ||
getLogger() | ||
.withError(error) | ||
.error(`[NetlifyCacheHandler]: Purging the cache for tag ${tag} failed`) | ||
}), | ||
) | ||
} | ||
} | ||
}) | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
async revalidateTag(tagOrTags: string | string[], ...args: any) { | ||
const revalidateTagPromise = this.doRevalidateTag(tagOrTags, ...args) | ||
|
||
const requestContext = getRequestContext() | ||
if (requestContext) { | ||
requestContext.trackBackgroundWork(revalidateTagPromise) | ||
} | ||
|
||
return revalidateTagPromise | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
private async doRevalidateTag(tagOrTags: string | string[], ...args: any) { | ||
Comment on lines
343
to
+355
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. App Router |
||
getLogger().withFields({ tagOrTags, args }).debug('NetlifyCacheHandler.revalidateTag') | ||
|
||
const tags = Array.isArray(tagOrTags) ? tagOrTags : [tagOrTags] | ||
|
@@ -314,7 +328,7 @@ export class NetlifyCacheHandler implements CacheHandler { | |
}), | ||
) | ||
|
||
purgeCache({ tags }).catch((error) => { | ||
await purgeCache({ tags }).catch((error) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this doesn't change much, because |
||
// TODO: add reporting here | ||
getLogger() | ||
.withError(error) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,34 @@ | ||
import type { ServerResponse } from 'node:http' | ||
import { isPromise } from 'node:util/types' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TIL |
||
|
||
import type { NextApiResponse } from 'next' | ||
|
||
import type { RequestContext } from './handlers/request-context.cjs' | ||
|
||
type ResRevalidateMethod = NextApiResponse['revalidate'] | ||
|
||
function isRevalidateMethod( | ||
key: string, | ||
nextResponseField: unknown, | ||
): nextResponseField is ResRevalidateMethod { | ||
return key === 'revalidate' && typeof nextResponseField === 'function' | ||
} | ||
|
||
// Needing to proxy the response object to intercept the revalidate call for on-demand revalidation on page routes | ||
export const nextResponseProxy = (res: ServerResponse, requestContext: RequestContext) => { | ||
return new Proxy(res, { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
get(target: any[string], key: string) { | ||
const originalValue = target[key] | ||
if (key === 'revalidate') { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
return async function newRevalidate(...args: any[]) { | ||
get(target: ServerResponse, key: string) { | ||
const originalValue = Reflect.get(target, key) | ||
if (isRevalidateMethod(key, originalValue)) { | ||
return function newRevalidate(...args: Parameters<ResRevalidateMethod>) { | ||
requestContext.didPagesRouterOnDemandRevalidate = true | ||
return originalValue?.apply(target, args) | ||
|
||
const result = originalValue.apply(target, args) | ||
if (result && isPromise(result)) { | ||
requestContext.trackBackgroundWork(result) | ||
} | ||
|
||
return result | ||
} | ||
} | ||
return originalValue | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,9 @@ | ||
export default async function handler(req, res) { | ||
try { | ||
await res.revalidate('/static/revalidate-manual') | ||
// res.revalidate returns a promise that can be awaited to wait for the revalidation to complete | ||
// if user doesn't await it, we still want to ensure the revalidation is completed, so we internally track | ||
// this as "background work" to ensure it completes before function suspends execution | ||
res.revalidate('/static/revalidate-manual') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this might make our page router on-demand revalidate tests flaky, as we do use to trigger on-demand revalidation, then sleep for a bit and then check revalidated page, but because we don't await here, the response from revalidate api endpoint would happen before revalidation completed (at least Next.js side of it) on the other hand it would be good to verify that background tracking actually works so I would prefer to have this and potentially increase sleep timeouts in tests (?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO it would be better to explicitly not await this call in a specific fixture/test for this behavior, and await it in all other tests. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it makes sense to have 2 cases here so in case those e2e tests fail it's easier to tell wether there is overall problem with revalidation or specifically with not awaited revalidation. Adding variants here would be much smoother after #2495 is in as it does add ~ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dd43ce8 added separate case for not awaiting |
||
return res.json({ code: 200, message: 'success' }) | ||
} catch (err) { | ||
return res.status(500).send({ code: 500, message: err.message }) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
making sure we track
purgeCache
part of the pages router on-demand revalidation so it completes before function execution gets suspended