-
-
Notifications
You must be signed in to change notification settings - Fork 329
fix(docs-cache): sync refresh + nightly prune of orphaned rows #839
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
base: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import type { Config } from '@netlify/functions' | ||
| import { pruneOldCacheEntries } from '~/utils/github-content-cache.server' | ||
|
|
||
| const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000 | ||
|
|
||
| const handler = async (req: Request) => { | ||
| const { next_run } = await req.json() | ||
|
|
||
| console.log('[cleanup-docs-cache] Starting docs cache prune...') | ||
|
|
||
| const startTime = Date.now() | ||
|
|
||
| try { | ||
| const { contentDeleted, artifactDeleted, threshold } = | ||
| await pruneOldCacheEntries(THIRTY_DAYS_MS) | ||
|
|
||
| const duration = Date.now() - startTime | ||
| console.log( | ||
| `[cleanup-docs-cache] Completed in ${duration}ms - Deleted ${contentDeleted.toLocaleString()} content rows and ${artifactDeleted.toLocaleString()} artifact rows older than ${threshold.toISOString()}`, | ||
| ) | ||
| console.log('[cleanup-docs-cache] Next invocation at:', next_run) | ||
| } catch (error) { | ||
| const duration = Date.now() - startTime | ||
| const errorMessage = error instanceof Error ? error.message : String(error) | ||
| const errorStack = error instanceof Error ? error.stack : undefined | ||
|
|
||
| console.error( | ||
| `[cleanup-docs-cache] Failed after ${duration}ms:`, | ||
| errorMessage, | ||
| ) | ||
| if (errorStack) { | ||
| console.error('[cleanup-docs-cache] Stack:', errorStack) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export default handler | ||
|
|
||
| export const config: Config = { | ||
| schedule: '0 3 * * *', | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,4 @@ | ||||||||||||||||
| import { and, eq, sql } from 'drizzle-orm' | ||||||||||||||||
| import { and, eq, lt, sql } from 'drizzle-orm' | ||||||||||||||||
| import { db } from '~/db/client' | ||||||||||||||||
| import { | ||||||||||||||||
| docsArtifactCache, | ||||||||||||||||
|
|
@@ -42,12 +42,6 @@ function isFresh(staleAt: Date) { | |||||||||||||||
| return staleAt.getTime() > Date.now() | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| function queueRefresh(key: string, fn: () => Promise<unknown>) { | ||||||||||||||||
| void withPendingRefresh(key, fn).catch((error) => { | ||||||||||||||||
| console.error(`[GitHub Cache] Failed to refresh ${key}:`, error) | ||||||||||||||||
| }) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| function readStoredTextValue(row: GithubContentCache | undefined) { | ||||||||||||||||
| if (!row) { | ||||||||||||||||
| return undefined | ||||||||||||||||
|
|
@@ -166,19 +160,8 @@ async function getCachedGitHubContent<T>(opts: { | |||||||||||||||
| const cachedRow = await readRow() | ||||||||||||||||
| const storedValue = opts.readStoredValue(cachedRow) | ||||||||||||||||
|
|
||||||||||||||||
| if (storedValue !== undefined) { | ||||||||||||||||
| if (cachedRow && isFresh(cachedRow.staleAt)) { | ||||||||||||||||
| return storedValue | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| if (storedValue !== null) { | ||||||||||||||||
| queueRefresh(opts.cacheKey, async () => { | ||||||||||||||||
| const value = await opts.origin() | ||||||||||||||||
| await persist(value) | ||||||||||||||||
| }) | ||||||||||||||||
|
|
||||||||||||||||
| return storedValue | ||||||||||||||||
| } | ||||||||||||||||
| if (storedValue !== undefined && cachedRow && isFresh(cachedRow.staleAt)) { | ||||||||||||||||
| return storedValue | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| return withPendingRefresh(opts.cacheKey, async () => { | ||||||||||||||||
|
|
@@ -297,16 +280,7 @@ export async function getCachedDocsArtifact<T>(opts: { | |||||||||||||||
| const storedValue = | ||||||||||||||||
| cachedRow && opts.isValue(cachedRow.payload) ? cachedRow.payload : undefined | ||||||||||||||||
|
|
||||||||||||||||
| if (storedValue !== undefined) { | ||||||||||||||||
| if (cachedRow && isFresh(cachedRow.staleAt)) { | ||||||||||||||||
| return storedValue | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| queueRefresh(cacheKey, async () => { | ||||||||||||||||
| const payload = await opts.build() | ||||||||||||||||
| await upsertDocsArtifact({ ...opts, payload }) | ||||||||||||||||
| }) | ||||||||||||||||
|
|
||||||||||||||||
| if (storedValue !== undefined && cachedRow && isFresh(cachedRow.staleAt)) { | ||||||||||||||||
| return storedValue | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
|
|
@@ -382,6 +356,27 @@ export async function markGitHubContentStale( | |||||||||||||||
| return rowCount | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| export async function pruneOldCacheEntries(olderThanMs: number) { | ||||||||||||||||
| const threshold = new Date(Date.now() - olderThanMs) | ||||||||||||||||
|
Comment on lines
+359
to
+360
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. Guard A non-finite or non-positive Suggested fix export async function pruneOldCacheEntries(olderThanMs: number) {
+ if (!Number.isFinite(olderThanMs) || olderThanMs <= 0) {
+ throw new Error('olderThanMs must be a positive finite number')
+ }
const threshold = new Date(Date.now() - olderThanMs)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
|
|
||||||||||||||||
| const [contentDeleted, artifactDeleted] = await Promise.all([ | ||||||||||||||||
| db | ||||||||||||||||
| .delete(githubContentCache) | ||||||||||||||||
| .where(lt(githubContentCache.updatedAt, threshold)) | ||||||||||||||||
| .returning({ repo: githubContentCache.repo }), | ||||||||||||||||
| db | ||||||||||||||||
| .delete(docsArtifactCache) | ||||||||||||||||
| .where(lt(docsArtifactCache.updatedAt, threshold)) | ||||||||||||||||
| .returning({ repo: docsArtifactCache.repo }), | ||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||
| ]) | ||||||||||||||||
|
Comment on lines
+362
to
+371
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. Avoid returning every deleted row just to compute counts. Using 🤖 Prompt for AI Agents |
||||||||||||||||
|
|
||||||||||||||||
| return { | ||||||||||||||||
| contentDeleted: contentDeleted.length, | ||||||||||||||||
| artifactDeleted: artifactDeleted.length, | ||||||||||||||||
| threshold, | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| export async function markDocsArtifactsStale( | ||||||||||||||||
| opts: { | ||||||||||||||||
| gitRef?: string | ||||||||||||||||
|
|
||||||||||||||||
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.
🧩 Analysis chain
🌐 Web query:
For@netlify/functions` v5.1.5 scheduled functions:next_runguaranteed for every invocation?Responserecommended for scheduled handlers?`💡 Result:
Citations:
🏁 Script executed:
cat -n netlify/functions/cleanup-docs-cache-background.ts | head -50Repository: TanStack/tanstack.com
Length of output: 1670
Move body parsing inside
tryand rethrow errors to mark failures.await req.json()(line 7) can throw before error handling, and thecatchblock currently swallows errors without rethrowing, which silently hides failed prune runs. Move JSON parsing inside thetryblock and rethrow in thecatchblock to ensure Netlify marks the invocation as failed when an error occurs.Suggested fix
const handler = async (req: Request) => { - const { next_run } = await req.json() - - console.log('[cleanup-docs-cache] Starting docs cache prune...') - const startTime = Date.now() + console.log('[cleanup-docs-cache] Starting docs cache prune...') try { + const { next_run } = await req.json() + const { contentDeleted, artifactDeleted, threshold } = await pruneOldCacheEntries(THIRTY_DAYS_MS) const duration = Date.now() - startTime console.log( `[cleanup-docs-cache] Completed in ${duration}ms - Deleted ${contentDeleted.toLocaleString()} content rows and ${artifactDeleted.toLocaleString()} artifact rows older than ${threshold.toISOString()}`, ) console.log('[cleanup-docs-cache] Next invocation at:', next_run) } catch (error) { const duration = Date.now() - startTime const errorMessage = error instanceof Error ? error.message : String(error) const errorStack = error instanceof Error ? error.stack : undefined console.error( `[cleanup-docs-cache] Failed after ${duration}ms:`, errorMessage, ) if (errorStack) { console.error('[cleanup-docs-cache] Stack:', errorStack) } + throw error } }📝 Committable suggestion
🤖 Prompt for AI Agents