Skip to content
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

perf: memoize blobs requests in the request scope #2777

Merged

Conversation

pieh
Copy link
Contributor

@pieh pieh commented Mar 19, 2025

Description

If site is doing multiple fetches with data caching enabled or is calling method wrapped in unstable_cache resulting in same cache key multiple times to render single page - we currently are repeatedly calling blobStore.get for each of the repeated use.

This PR looks to add memoization of those calls scoped to concrete request. We don't want to memoize them globally due to distributed nature of serverless with potentially many instances running at the same time, each holding different potentially state, but we should be able to safely memoize blobs call while handling same request. This also allow for reading it's own writes within scope of same request handling.

This PR also replace similar handling we already had for tag manifests with more generic one shared for all blobs operations.

Tests

  • Added fetch specific integration test asserting we only fetch same thing from blobs once per request
  • Added memoized blob store unit tests
  • We already did have tests specific to tag manifests that I will not change, but they should continue to pass ( for example
    expect(
    getBlobServerGets(ctx, isTagManifest),
    `expected tag manifests to be retrieved at most once per tag`,
    ).toBeDistinct()
    ctx.blobServerGetSpy.mockClear()
    )

Relevant links (GitHub issues, etc.) or a picture of cute animal

https://linear.app/netlify/issue/FRB-1629/intermittent-proxy-timeouts-on-runtime-v594

…asserting we only check blobs once per request for same key
Copy link

github-actions bot commented Mar 19, 2025

📊 Package size report   0.9%↑

File Before (Size / Gzip) After (Size / Gzip)
dist/esm-chunks/package-KUKZOWON.js 3.6 kB / 1.4 kB
dist/esm-chunks/package-N5UKFZQ4.js 3.7 kB / 1.4 kB
dist/run/config.js 1.2 kB / 595 B 12%↑1.3 kB / 8%↑643 B
dist/run/handlers/cache.cjs 22.0 kB / 5.8 kB -5.43%↓20.8 kB / -3.04%↓5.6 kB
dist/run/handlers/request-context.cjs 5.8 kB / 1.8 kB 6%↑6.2 kB / 5%↑1.9 kB
dist/run/handlers/server.js 141.4 kB / 33.2 kB -0.01%↓141.4 kB / -0.05%↓33.2 kB
dist/run/handlers/tracer.cjs 29.9 kB / 6.3 kB 1%↑30.2 kB / 2%↑6.3 kB
dist/run/headers.js 8.0 kB / 2.4 kB -5.11%↓7.6 kB / -5.38%↓2.3 kB
dist/run/next.cjs 23.6 kB / 5.8 kB -0.42%↓23.5 kB / -0.55%↓5.8 kB
dist/run/regional-blob-store.cjs 21.3 kB / 6.1 kB
dist/run/storage/regional-blob-store.cjs 21.3 kB / 6.1 kB
dist/run/storage/request-scoped-in-memory-cache.cjs 46.3 kB / 10.7 kB
dist/run/storage/storage.cjs 4.0 kB / 1.3 kB
dist/shared/blob-types.cjs 1.6 kB / 640 B
package.json 3.2 kB / 1.2 kB 0.9%↑3.2 kB / 0.8%↑1.2 kB
Total (Includes all files) 5.8 MB / 1.2 MB 0.9%↑5.9 MB / 1%↑1.2 MB
Tarball size 1.2 MB 0.9%↑1.2 MB
Unchanged files
File Size (Size / Gzip)
dist/build/advanced-api-routes.js 4.3 kB / 1.4 kB
dist/build/cache.js 1.0 kB / 414 B
dist/build/content/next-shims/telemetry-storage.cjs 1.6 kB / 659 B
dist/build/content/prerendered.js 9.4 kB / 2.8 kB
dist/build/content/server.js 8.7 kB / 2.8 kB
dist/build/content/static.js 4.1 kB / 1.4 kB
dist/build/functions/edge.js 20.8 kB / 5.6 kB
dist/build/functions/server.js 5.0 kB / 1.6 kB
dist/build/image-cdn.js 54.0 kB / 11.1 kB
dist/build/plugin-context.js 10.1 kB / 3.0 kB
dist/build/templates/handler-monorepo.tmpl.js 1.7 kB / 703 B
dist/build/templates/handler.tmpl.js 1.6 kB / 655 B
dist/build/verification.js 4.5 kB / 1.5 kB
dist/esm-chunks/chunk-5QSXBV7L.js 2.4 kB / 842 B
dist/esm-chunks/chunk-APO262HE.js 61.2 kB / 11.1 kB
dist/esm-chunks/chunk-GNGHTHMQ.js 55.6 kB / 9.7 kB
dist/esm-chunks/chunk-KGYJQ2U2.js 186.5 kB / 32.9 kB
dist/esm-chunks/chunk-OEQOKJGE.js 2.3 kB / 977 B
dist/index.js 3.4 kB / 1.1 kB
dist/run/constants.js 516 B / 308 B
dist/run/handlers/tracing.js 3.0 MB / 418.4 kB
dist/run/handlers/wait-until.cjs 1.4 kB / 665 B
dist/run/revalidate.js 1.0 kB / 475 B
dist/shared/blobkey.js 742 B / 399 B
dist/shared/cache-types.cjs 1.3 kB / 566 B
edge-runtime/lib/headers.ts 1.9 kB / 841 B
edge-runtime/lib/logging.ts 115 B / 121 B
edge-runtime/lib/middleware.ts 1.9 kB / 807 B
edge-runtime/lib/next-request.ts 3.3 kB / 1.1 kB
edge-runtime/lib/response.ts 9.2 kB / 2.9 kB
edge-runtime/lib/routing.ts 15.1 kB / 3.9 kB
edge-runtime/lib/util.test.ts 1.6 kB / 356 B
edge-runtime/lib/util.ts 3.7 kB / 1.3 kB
edge-runtime/matchers.json 3 B / 23 B
edge-runtime/middleware.ts 2.4 kB / 1.0 kB
edge-runtime/next.config.json 3 B / 23 B
edge-runtime/README.md 992 B / 509 B
edge-runtime/shim/index.js 1.5 kB / 717 B
edge-runtime/vendor.ts 745 B / 312 B
edge-runtime/vendor/deno.land/[email protected]/_util/asserts.ts 854 B / 461 B
edge-runtime/vendor/deno.land/[email protected]/_util/os.ts 644 B / 355 B
edge-runtime/vendor/deno.land/[email protected]/async/abortable.ts 4.0 kB / 1.0 kB
edge-runtime/vendor/deno.land/[email protected]/async/deadline.ts 974 B / 544 B
edge-runtime/vendor/deno.land/[email protected]/async/debounce.ts 2.2 kB / 956 B
edge-runtime/vendor/deno.land/[email protected]/async/deferred.ts 1.5 kB / 798 B
edge-runtime/vendor/deno.land/[email protected]/async/delay.ts 1.8 kB / 845 B
edge-runtime/vendor/deno.land/[email protected]/async/mod.ts 465 B / 241 B
edge-runtime/vendor/deno.land/[email protected]/async/mux_async_iterator.ts 2.5 kB / 1.1 kB
edge-runtime/vendor/deno.land/[email protected]/async/pool.ts 3.2 kB / 1.4 kB
edge-runtime/vendor/deno.land/[email protected]/async/retry.ts 2.4 kB / 1.0 kB
edge-runtime/vendor/deno.land/[email protected]/async/tee.ts 2.1 kB / 924 B
edge-runtime/vendor/deno.land/[email protected]/bytes/index_of_needle.ts 1.4 kB / 668 B
edge-runtime/vendor/deno.land/[email protected]/crypto/timing_safe_equal.ts 875 B / 442 B
edge-runtime/vendor/deno.land/[email protected]/datetime/to_imf.ts 1.3 kB / 681 B
edge-runtime/vendor/deno.land/[email protected]/encoding/base64.ts 2.5 kB / 1.0 kB
edge-runtime/vendor/deno.land/[email protected]/encoding/base64url.ts 2.0 kB / 872 B
edge-runtime/vendor/deno.land/[email protected]/flags/mod.ts 22.6 kB / 5.9 kB
edge-runtime/vendor/deno.land/[email protected]/fmt/colors.ts 12.4 kB / 2.7 kB
edge-runtime/vendor/deno.land/[email protected]/fmt/printf.ts 27.7 kB / 7.7 kB
edge-runtime/vendor/deno.land/[email protected]/http/cookie.ts 11.5 kB / 3.6 kB
edge-runtime/vendor/deno.land/[email protected]/node/_core.ts 2.3 kB / 716 B
edge-runtime/vendor/deno.land/[email protected]/node/_events.d.ts 27.2 kB / 5.8 kB
edge-runtime/vendor/deno.land/[email protected]/node/_events.mjs 28.0 kB / 7.4 kB
edge-runtime/vendor/deno.land/[email protected]/node/_global.d.ts 1.7 kB / 650 B
edge-runtime/vendor/deno.land/[email protected]/node/_next_tick.ts 5.0 kB / 1.4 kB
edge-runtime/vendor/deno.land/[email protected]/node/_process/exiting.ts 138 B / 138 B
edge-runtime/vendor/deno.land/[email protected]/node/_process/process.ts 3.8 kB / 1.4 kB
edge-runtime/vendor/deno.land/[email protected]/node/_process/stdio.mjs 336 B / 233 B
edge-runtime/vendor/deno.land/[email protected]/node/_process/streams.mjs 4.0 kB / 1.4 kB
edge-runtime/vendor/deno.land/[email protected]/node/_stream.d.ts 53.2 kB / 11.9 kB
edge-runtime/vendor/deno.land/[email protected]/node/_stream.mjs 91.2 kB / 25.4 kB
edge-runtime/vendor/deno.land/[email protected]/node/_util/_util_callbackify.ts 4.3 kB / 1.7 kB
edge-runtime/vendor/deno.land/[email protected]/node/_utils.ts 5.9 kB / 2.0 kB
edge-runtime/vendor/deno.land/[email protected]/node/assert.ts 23.1 kB / 4.4 kB
edge-runtime/vendor/deno.land/[email protected]/node/assertion_error.ts 19.6 kB / 6.1 kB
edge-runtime/vendor/deno.land/[email protected]/node/async_hooks.ts 7.7 kB / 2.1 kB
edge-runtime/vendor/deno.land/[email protected]/node/buffer.ts 262 B / 204 B
edge-runtime/vendor/deno.land/[email protected]/node/events.ts 303 B / 221 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/_libuv_winerror.ts 7.8 kB / 1.9 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/_listen.ts 561 B / 342 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/_node.ts 443 B / 335 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/_timingSafeEqual.ts 479 B / 268 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/_utils.ts 2.4 kB / 938 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/_winerror.ts 354.4 kB / 64.4 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/ares.ts 2.4 kB / 1.1 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/async_wrap.ts 4.0 kB / 1.8 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/buffer.ts 3.5 kB / 1.3 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/cares_wrap.ts 15.2 kB / 3.9 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/config.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/connection_wrap.ts 2.6 kB / 1.3 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/constants.ts 21.5 kB / 5.1 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/contextify.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/credentials.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/crypto.ts 448 B / 244 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/errors.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/fs_dir.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/fs_event_wrap.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/fs.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/handle_wrap.ts 1.8 kB / 1.0 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/heap_utils.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/http_parser.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/icu.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/inspector.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/js_stream.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/messaging.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/mod.ts 3.1 kB / 955 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/module_wrap.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/native_module.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/natives.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/node_file.ts 2.9 kB / 1.5 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/node_options.ts 1.8 kB / 989 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/options.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/os.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/performance.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/pipe_wrap.ts 10.4 kB / 3.3 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/process_methods.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/report.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/serdes.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/signal_wrap.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/spawn_sync.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/stream_wrap.ts 9.3 kB / 2.8 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/string_decoder.ts 504 B / 261 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/symbols.ts 1.4 kB / 828 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/task_queue.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/tcp_wrap.ts 13.1 kB / 3.7 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/timers.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/tls_wrap.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/trace_events.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/tty_wrap.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/types.ts 5.7 kB / 1.4 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/udp_wrap.ts 12.4 kB / 3.6 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/url.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/util.ts 4.0 kB / 1.8 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/uv.ts 20.1 kB / 3.8 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/v8.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/worker.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal_binding/zlib.ts 87 B / 104 B
edge-runtime/vendor/deno.land/[email protected]/node/internal/buffer.d.ts 73.6 kB / 12.1 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal/buffer.mjs 66.1 kB / 10.6 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal/crypto/_keys.ts 463 B / 262 B
edge-runtime/vendor/deno.land/[email protected]/node/internal/crypto/constants.ts 252 B / 173 B
edge-runtime/vendor/deno.land/[email protected]/node/internal/error_codes.ts 322 B / 250 B
edge-runtime/vendor/deno.land/[email protected]/node/internal/errors.ts 78.9 kB / 17.4 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal/fixed_queue.ts 4.4 kB / 1.2 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal/hide_stack_frames.ts 550 B / 377 B
edge-runtime/vendor/deno.land/[email protected]/node/internal/net.ts 3.1 kB / 1.5 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal/normalize_encoding.mjs 2.1 kB / 500 B
edge-runtime/vendor/deno.land/[email protected]/node/internal/options.ts 1.7 kB / 959 B
edge-runtime/vendor/deno.land/[email protected]/node/internal/primordials.mjs 1.8 kB / 431 B
edge-runtime/vendor/deno.land/[email protected]/node/internal/process/per_thread.mjs 7.8 kB / 2.3 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal/readline/callbacks.mjs 3.8 kB / 1.4 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal/readline/utils.mjs 14.3 kB / 3.7 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal/streams/destroy.mjs 6.9 kB / 1.8 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal/streams/end-of-stream.mjs 7.1 kB / 1.9 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal/streams/utils.mjs 5.9 kB / 1.2 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal/util.mjs 4.0 kB / 1.4 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal/util/comparisons.ts 16.6 kB / 3.8 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal/util/debuglog.ts 3.2 kB / 1.4 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal/util/inspect.mjs 71.5 kB / 19.8 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal/util/types.ts 3.7 kB / 1.3 kB
edge-runtime/vendor/deno.land/[email protected]/node/internal/validators.mjs 8.0 kB / 2.1 kB
edge-runtime/vendor/deno.land/[email protected]/node/process.ts 19.4 kB / 5.2 kB
edge-runtime/vendor/deno.land/[email protected]/node/stream.ts 671 B / 346 B
edge-runtime/vendor/deno.land/[email protected]/node/string_decoder.ts 10.3 kB / 3.3 kB
edge-runtime/vendor/deno.land/[email protected]/node/util.ts 7.8 kB / 2.2 kB
edge-runtime/vendor/deno.land/[email protected]/node/util/types.ts 199 B / 153 B
edge-runtime/vendor/deno.land/[email protected]/path/_constants.ts 2.0 kB / 727 B
edge-runtime/vendor/deno.land/[email protected]/path/_interface.ts 728 B / 369 B
edge-runtime/vendor/deno.land/[email protected]/path/_util.ts 5.0 kB / 1.6 kB
edge-runtime/vendor/deno.land/[email protected]/path/common.ts 1.2 kB / 607 B
edge-runtime/vendor/deno.land/[email protected]/path/glob.ts 12.7 kB / 3.9 kB
edge-runtime/vendor/deno.land/[email protected]/path/mod.ts 1.4 kB / 690 B
edge-runtime/vendor/deno.land/[email protected]/path/posix.ts 13.9 kB / 3.7 kB
edge-runtime/vendor/deno.land/[email protected]/path/separator.ts 259 B / 209 B
edge-runtime/vendor/deno.land/[email protected]/path/win32.ts 28.5 kB / 6.4 kB
edge-runtime/vendor/deno.land/[email protected]/streams/write_all.ts 2.2 kB / 598 B
edge-runtime/vendor/deno.land/[email protected]/testing/_diff.ts 11.6 kB / 3.6 kB
edge-runtime/vendor/deno.land/[email protected]/testing/_format.ts 705 B / 462 B
edge-runtime/vendor/deno.land/[email protected]/testing/asserts.ts 25.5 kB / 5.7 kB
edge-runtime/vendor/deno.land/[email protected]/types.d.ts 4.2 kB / 1.2 kB
edge-runtime/vendor/deno.land/x/[email protected]/pkg/htmlrewriter_bg.wasm 573.2 kB / 262.7 kB
edge-runtime/vendor/deno.land/x/[email protected]/pkg/htmlrewriter.js 31.0 kB / 4.7 kB
edge-runtime/vendor/deno.land/x/[email protected]/src/index.ts 2.6 kB / 989 B
edge-runtime/vendor/deno.land/x/[email protected]/src/types.d.ts 2.1 kB / 446 B
edge-runtime/vendor/deno.land/x/[email protected]/index.ts 15.4 kB / 4.2 kB
edge-runtime/vendor/import_map.json 148 B / 111 B
edge-runtime/vendor/v1-7-0--edge-utils.netlify.app/logger/logger.ts 3.2 kB / 747 B
edge-runtime/vendor/v1-7-0--edge-utils.netlify.app/logger/mod.ts 29 B / 49 B
LICENSE 1.1 kB / 661 B
manifest.yml 31 B / 51 B
README.md 2.8 kB / 1.2 kB

🤖 This report was automatically generated by pkg-size-action

@pieh pieh changed the title perf: memoize blobs requests in the scope of same request perf: memoize blobs requests in the request scope Mar 19, 2025
@pieh pieh force-pushed the michalpiechowiak/frb-1629-intermittent-proxy-timeouts-on-runtime-v594 branch 3 times, most recently from f8a565c to c97fee2 Compare March 20, 2025 15:50
@pieh pieh added the test all versions Run e2e tests against old and canary versions of Next.js label Mar 20, 2025
@pieh pieh force-pushed the michalpiechowiak/frb-1629-intermittent-proxy-timeouts-on-runtime-v594 branch from c97fee2 to 85d4eb9 Compare March 21, 2025 07:16
@pieh pieh marked this pull request as ready for review March 21, 2025 10:42

const FETCH_BEFORE_NEXT_PATCHED_IT = Symbol.for('nf-not-patched-fetch')
const IN_MEMORY_CACHE_MAX_SIZE = Symbol.for('nf-in-memory-cache-max-size')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would a customer make a call on this number? Trail and error?
Should we maybe just use DEFAULT_FALLBACK_MAX_SIZE until there's an observed need to increase this?

Copy link
Contributor Author

@pieh pieh Mar 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Customer wouldn't use this directly. Idea was to honor actual next.config.js setting https://nextjs.org/docs/app/api-reference/config/next-config-js/incrementalCacheHandlerPath (cacheMaxMemorySize one) that I do wire up here -

// honor the in-memory cache size from next.config (either one set by user or Next.js default)
setInMemoryCacheMaxSizeFromNextConfig(
config.cacheMaxMemorySize ?? config.experimental?.isrMemoryCacheSize,
)

Note checking 2 settings because vercel/next.js#57953 landed in 14.1.0 that moved experimental one into stable (and renamed it)

This comment might shed a bit more light on few following ones

: DEFAULT_FALLBACK_MAX_SIZE

extendedGlobalThis[IN_MEMORY_LRU_CACHE] =
maxSize === 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the user could have IN_MEMORY_LRU_CACHE set, but IN_MEMORY_CACHE_MAX_SIZE set to 0 which would keep it off. Two dials like this probably warrants a warning message or similar for the user

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All those extendedGlobalThis are meant to be private handling and not public - users are not meant to mess with them

globalThis is used here not as a way for user to set things, but rather in case we end up with duplicate of this module (like we did prior to #2774 )

This is also similar setup that you can spot in few places in Next.js itself (like https://github.com/vercel/next.js/blob/9a5b5ce5cef468a14f5cd2438a6e667dcb7d7996/packages/next/src/server/use-cache/handlers.ts#L14-L26 for example)

const extendedGlobalThis = globalThis as typeof globalThis & {
[FETCH_BEFORE_NEXT_PATCHED_IT]?: typeof globalThis.fetch
[IN_MEMORY_CACHE_MAX_SIZE]?: number
[IN_MEMORY_LRU_CACHE]?: BlobLRUCache | null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move noOpInMemoryCache here? Instead of null?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized these are types, but the intention of my comment still sort of stands. Can we try to have these be non-nullable if possible?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noOpInMemoryCache is not implementing same interface as BlobLRUCache so it couldn't be used here. To make it non-nullable I would need to create a shim class that implements LRUCache interface and this doesn't seem worth doing and maintaing to get rid of null case to me?

extendedGlobalThis[IN_MEMORY_LRU_CACHE] =
maxSize === 0
? null // if user sets 0 in their config, we should honor that and not use in-memory cache
: new LRUCache<string, BlobType | typeof NullValue | Promise<BlobType | null>>({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is LRUCache a lot better than just using {} for example?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LRUCache ensures some limits, so that lambda that stays warm for a long time doesn't suddenly OOM. The memory/size is not exact (hence naming of estimate for the size calculation) and should be thought more as "ballpark" values.

If we don't use LRUCache - we'd need to come up with some other mechanism to manage in-memory pruning, but I don't think it's worth doing given how battle-tested lru-cache package is (with 200 million downloads a wek).

Lastly - default implementation for self-hosted Next.js also uses LRUCache for its in-memory cache (size calculation was inspired by how it's used in that default implementation)

},
}

const getRequestSpecificInMemoryCache = (): RequestSpecificInMemoryCache => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a interface/wrapper around LRUCache, should we pop this in another file? Perhaps in-memory-cache.cts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a "refactor" in cff9f73 that did few things:

  1. I did create run/storage directory to not pollute run with "implementation details" of storage handling
  2. There is "public" module run/storage/storage.ts that is meant to be used everywhere and other modules in run/storage should not be imported directly outside of storage directory to not leak abstractions/implementation details (added eslint rule for that)
  3. regional-blob-store.cts is now exactly as it is in main branch - it was just moved (with adjusted import paths only) - this now is not meant to be used directly because it's considered implementation detail
  4. created request-scoped-in-memory-cache.cts and moved details of in-memory handling there (this is also implementation detail not meant to be used directly outside of run/storage)

Hopefully this splits concerns reasonably well, making modules rather small and hopefully easier to digest and provide some boundaries between concerns with standard import/export contract

return false
}

const isHtmlBlob = (value: BlobType): value is HtmlBlob => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it belongs in another file as it is not a "cache type"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is more of possible value that is being stored in blobs, but not exactly cache type. TagManifest is also same way, because it's also not really a cache type (it's not comming from next.js - it's our custom handling for tracking tag invalidations).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did some shuffling in ca233ef - i did create sibling module blob-types and moved those non Next.js CacheHandler related types and type guards there. Actually fixing type guards ( #2777 (comment) ) will be done in separate commit

The main thing with this move to me was question about estimateBlobSize and wether this should be in this new module or it should be in the in-memory module, but decided to move it to in-memory module because size is only concern/detail of in-memory cache and not shared anywhere else (at least not right now). Maybe it could have it's own module, but to me over-modularizing make things less readable when you have to jump too much between different modules 🤷

return false
}

export const estimateBlobSize = (valueToStore: BlobType | null | Promise<unknown>): number => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, this is not a "cache type" so I think we should put this in a different file

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -89,3 +89,16 @@ export function getTracer(): RuntimeTracer {

return tracer
}

export function recordWarning(warning: Error, span?: Span) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getCacheKeySpan looked to be non-nullable, can we make span non-nullable here as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

recordWarning(
new Error(
`Blob size calculation did fallback to JSON.stringify. Kind: KnownKindFailed: ${knownKindFailed}, ${valueToStore.value?.kind ?? 'undefined'}`,
),
)
this call doesn't have access to span to pass it, so that's why I made it optional.

Generally preferably we do pass span explicitly instead of getting "active span", because it's more stable on which span things would be recorded, but it's better to record at all than skip recording because we don't have reference to a span.

Alternative could be to make above linked code get active span and then make span here non-nullable, but I did want to simplify usage of it, but would be fine making that change (less "magic")

Copy link
Contributor Author

@pieh pieh Mar 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did attempt the alternative but to me it moved too much tracing related logic and checks to module that doesn't already have reference to containing span I mentioned above and concern of finding active span seems much more in tracer.cts module than modules that might want to record warnings, so I'd prefer to keep the signature of this function as it is

{
// only */storage/storage.cjs is allowed to be imported
// rest are implementation details that should not be used directly
group: ['*/storage/*', '!*/storage/storage.cjs'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use storage/index.cjs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally prefer not to because my personal experience is that using index modules makes navigation around code base harder when you start to get multiple modules with same name in different directories, as well as just seeing multiple index modules opened in IDE tabs hellish so I do try to avoid it

similar example (just instead of index module, is Next's app router pattern to have page.j/ts(x)):
image

I can appreciate that storage/storage.cjs looks pretty weird, but I think it's better than added mental load of tracking multiple index modules. Additionally with IDEs auto-adding imports (like https://code.visualstudio.com/docs/languages/javascript#_auto-imports ) it's a bit "out of mind" as nowadays you usually don't even write your import statements manually

Some compromise could be to have index that just re-exports and doesn't actually have any logic inside, but then it just pollute directory tree IMO

import { nextResponseProxy } from '../revalidate.js'
import { setFetchBeforeNextPatchedIt } from '../storage/storage.cjs'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing to change here, just reading notes - Both regional-blob-store and storage are unexpected places for me to import setFetchBeforeNextPatchedIt from, but don't have a suggestion for an alterantive at this time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only alternative really would be to not call this function (and just remove this function altogether) and instead directly store fetch on globalThis to be consumed later

const IN_MEMORY_LRU_CACHE = Symbol.for('nf-in-memory-lru-cache')
const extendedGlobalThis = globalThis as typeof globalThis & {
[IN_MEMORY_CACHE_MAX_SIZE]?: number
[IN_MEMORY_LRU_CACHE]?: BlobLRUCache | null
Copy link
Contributor

@mrstork mrstork Mar 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting for myself that we're actually using null and undefined as distinct values

  • undefined means the BlobLRUCache was not initialized yet
  • null means the BlobLRUCache was initialized but resulted in no value

}

interface RequestScopedInMemoryCache {
get(key: string): BlobType | null | Promise<BlobType | null> | undefined
Copy link
Contributor

@mrstork mrstork Mar 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting for myself that we're actually using null and undefined as distinct values

  • null means there was a NullValue in the cache/blob store
  • undefined means there was no value found, the RequestContext doesn't exist, or LRUCache is not specified

@mrstork mrstork force-pushed the michalpiechowiak/frb-1629-intermittent-proxy-timeouts-on-runtime-v594 branch from 49bee0f to af34d70 Compare March 27, 2025 20:02
@@ -476,7 +449,7 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
await Promise.all(
tags.map(async (tag) => {
try {
await this.blobStore.setJSON(await this.encodeBlobKey(tag), data)
await this.cacheStore.set(tag, data, 'tagManifest.set')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting that this will add tracing (I believe that's a good thing)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I did want to use this opportunity to capture all blobs operations for debugging purposes

const file = (await store.get(await encodeBlobKey(relPath), {
type: 'json',
})) as HtmlBlob | null
const file = await cacheStore.get<HtmlBlob>(relPath, 'staticHtml.get')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again noting that this will add tracing (again I think it's a good thing)

const file = (await store.get(await encodeBlobKey(relPath), {
type: 'json',
})) as HtmlBlob | null
const file = await cacheStore.get<HtmlBlob>(relPath, 'staticHtml.get')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably automatic, but I'm just calling it out in case - I see blobStore.set and tagManifest.set but not seeing staticHtml.set anywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We upload those only at build time (

await writeFile(
join(destDir, await encodeBlobKey(path)),
JSON.stringify({ html, isFallback } satisfies HtmlBlob),
'utf-8',
)
which will make use of File-based blobs uploads. As we only copy and let @nelify/cli or buildbot upload those blobs after build command - we don't create build-time traces for that.

We don't add/update those anymore once deployed

@mrstork mrstork force-pushed the michalpiechowiak/frb-1629-intermittent-proxy-timeouts-on-runtime-v594 branch from af34d70 to 7daeeca Compare March 27, 2025 20:45

// TODO: use metadata for this
lastModified = await tracer.withActiveSpan(
const cacheStore = getMemoizedKeyValueStoreBackedByRegionalBlobStore({ consistency: 'strong' })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming note - perhaps getMemoizedRegionalBlobStore or getRegionalBlobService?

inMemoryCache.set(key, getPromise)
return getPromise
},
async set(key: string, value: BlobType, otelSpanTitle: string) {
Copy link
Contributor

@mrstork mrstork Mar 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this works, but maybe this?

async set<T extends BlobType>(key: string, value: T, otelSpanTitle: string) {

My idea here is to have as similar an interface between get/set as we can

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only added generic to .get to save few characters as we currently any time we use blobStore.get we have to do something like this:

})) as NetlifyCacheHandlerValue | null

so it just avoid adding | null in all those places while preserving same type safety.

I don't think that there does need to be parity between .get and .set just for parity sake.

That said, adding generic to .set is fine and we could lock down to concrete types from available here

export type BlobType = NetlifyCacheHandlerValue | TagManifest | HtmlBlob
and it could technically increase type safety (so we don't ever try to set TagManifest when generally given code path should set NetlifyCacheHandlerValue), so if you feel up for it - you can go ahead and make that change.

return await encodeBlobKeyImpl(key)
}

export const getMemoizedKeyValueStoreBackedByRegionalBlobStore = (
Copy link
Contributor

@mrstork mrstork Mar 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hopeful that the 3 implementations (NetlifyCacheHandlerValue | TagManifest | HtmlBlob) of this don't diverge too much. If we find ourselves adding any if statements in here for each of the types, the responsible thing to do quickly becomes splitting off.

Copy link
Contributor Author

@pieh pieh Mar 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can move size estimation out of storage directory, we could add similar eslint rule like I did for "private storage modules" to not allow imports other than BlobType from the module that exports it in that directory to enforce storage not caring about details of individual types and only enforce that we use allowed union of types


// lru-cache types don't like using `null` for values, so we use a symbol to represent it and do conversion
// so it doesn't leak outside
const NullValue = Symbol.for('null-value')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a case today where we save null in the blob store?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answer - 404 pages in certain scenarios

@pieh pieh merged commit a2881bf into main Mar 28, 2025
190 of 192 checks passed
@pieh pieh deleted the michalpiechowiak/frb-1629-intermittent-proxy-timeouts-on-runtime-v594 branch March 28, 2025 09:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
test all versions Run e2e tests against old and canary versions of Next.js
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants