Skip to content

Commit 898f68a

Browse files
authored
Fix : incremental cache fixes (#257)
* potential fix for EMFILE, too many open file * fix dynamo provider not generated when no fetch cache * Add flags to disable dynamodb cache and incremental cache
1 parent 9251f67 commit 898f68a

File tree

4 files changed

+142
-67
lines changed

4 files changed

+142
-67
lines changed

packages/open-next/src/adapters/cache.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ import {
1616
// import { getDerivedTags } from "next/dist/server/lib/incremental-cache/utils";
1717
import path from "path";
1818

19-
import { loadBuildId } from "./config/util.js";
20-
import { awsLogger, debug, error } from "./logger.js";
19+
import { debug, error } from "./logger.js";
2120

2221
// TODO: Remove this, temporary only to run some tests
2322
const getDerivedTags = (tags: string[]) => tags;
@@ -105,30 +104,32 @@ type Extension =
105104
const {
106105
CACHE_BUCKET_NAME,
107106
CACHE_BUCKET_KEY_PREFIX,
108-
CACHE_BUCKET_REGION,
109107
CACHE_DYNAMO_TABLE,
108+
NEXT_BUILD_ID,
110109
} = process.env;
111110

111+
declare global {
112+
var S3Client: S3Client;
113+
var dynamoClient: DynamoDBClient;
114+
var disableDynamoDBCache: boolean;
115+
var disableIncrementalCache: boolean;
116+
}
117+
112118
export default class S3Cache {
113119
private client: S3Client;
114120
private dynamoClient: DynamoDBClient;
115121
private buildId: string;
116122

117123
constructor(_ctx: CacheHandlerContext) {
118-
this.client = new S3Client({
119-
region: CACHE_BUCKET_REGION,
120-
logger: awsLogger,
121-
});
122-
this.dynamoClient = new DynamoDBClient({
123-
region: CACHE_BUCKET_REGION,
124-
logger: awsLogger,
125-
});
126-
this.buildId = loadBuildId(
127-
path.dirname(_ctx.serverDistDir ?? ".next/server"),
128-
);
124+
this.client = globalThis.S3Client;
125+
this.dynamoClient = globalThis.dynamoClient;
126+
this.buildId = NEXT_BUILD_ID!;
129127
}
130128

131129
public async get(key: string, options?: boolean | { fetchCache?: boolean }) {
130+
if (globalThis.disableIncrementalCache) {
131+
return null;
132+
}
132133
const isFetchCache =
133134
typeof options === "object" ? options.fetchCache : options;
134135
const keys = await this.listS3Object(key);
@@ -257,6 +258,9 @@ export default class S3Cache {
257258
}
258259

259260
async set(key: string, data?: IncrementalCacheValue): Promise<void> {
261+
if (globalThis.disableIncrementalCache) {
262+
return;
263+
}
260264
if (data?.kind === "ROUTE") {
261265
const { body, status, headers } = data;
262266
await Promise.all([
@@ -317,6 +321,9 @@ export default class S3Cache {
317321
}
318322

319323
public async revalidateTag(tag: string) {
324+
if (globalThis.disableDynamoDBCache || globalThis.disableIncrementalCache) {
325+
return;
326+
}
320327
debug("revalidateTag", tag);
321328
// Find all keys with the given tag
322329
const paths = await this.getByTag(tag);
@@ -334,6 +341,7 @@ export default class S3Cache {
334341

335342
private async getTagsByPath(path: string) {
336343
try {
344+
if (disableDynamoDBCache) return [];
337345
const result = await this.dynamoClient.send(
338346
new QueryCommand({
339347
TableName: CACHE_DYNAMO_TABLE,
@@ -359,6 +367,7 @@ export default class S3Cache {
359367
//TODO: Figure out a better name for this function since it returns the lastModified
360368
private async getHasRevalidatedTags(key: string, lastModified?: number) {
361369
try {
370+
if (disableDynamoDBCache) return lastModified ?? Date.now();
362371
const result = await this.dynamoClient.send(
363372
new QueryCommand({
364373
TableName: CACHE_DYNAMO_TABLE,
@@ -387,6 +396,7 @@ export default class S3Cache {
387396

388397
private async getByTag(tag: string) {
389398
try {
399+
if (disableDynamoDBCache) return [];
390400
const { Items } = await this.dynamoClient.send(
391401
new QueryCommand({
392402
TableName: CACHE_DYNAMO_TABLE,
@@ -413,6 +423,7 @@ export default class S3Cache {
413423

414424
private async batchWriteDynamoItem(req: { path: string; tag: string }[]) {
415425
try {
426+
if (disableDynamoDBCache) return;
416427
await Promise.all(
417428
this.chunkArray(req, 25).map((Items) => {
418429
return this.dynamoClient.send(

packages/open-next/src/adapters/server-adapter.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
/* eslint-disable unused-imports/no-unused-imports */
2+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
3+
import { S3Client } from "@aws-sdk/client-s3";
4+
25
// We load every config here so that they are only loaded once
36
// and during cold starts
47
import {
@@ -13,13 +16,34 @@ import {
1316
PublicAssets,
1417
RoutesManifest,
1518
} from "./config/index.js";
19+
import { awsLogger } from "./logger.js";
1620
import { lambdaHandler } from "./plugins/lambdaHandler.js";
1721
import { setNodeEnv } from "./util.js";
1822

1923
setNodeEnv();
2024
setBuildIdEnv();
2125
setNextjsServerWorkingDirectory();
2226

27+
///////////////////////
28+
// AWS global client //
29+
///////////////////////
30+
31+
declare global {
32+
var S3Client: S3Client;
33+
var dynamoClient: DynamoDBClient;
34+
}
35+
36+
const CACHE_BUCKET_REGION = process.env.CACHE_BUCKET_REGION;
37+
38+
globalThis.S3Client = new S3Client({
39+
region: CACHE_BUCKET_REGION,
40+
logger: awsLogger,
41+
});
42+
globalThis.dynamoClient = new DynamoDBClient({
43+
region: CACHE_BUCKET_REGION,
44+
logger: awsLogger,
45+
});
46+
2347
/////////////
2448
// Handler //
2549
/////////////

packages/open-next/src/build.ts

Lines changed: 85 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@ import {
1313
import { minifyAll } from "./minimize-js.js";
1414
import openNextPlugin from "./plugin.js";
1515

16+
interface DangerousOptions {
17+
/**
18+
* The dynamo db cache is used for revalidateTags and revalidatePath.
19+
* @default false
20+
*/
21+
disableDynamoDBCache?: boolean;
22+
/**
23+
* The incremental cache is used for ISR and SSG.
24+
* Disable this only if you use only SSR
25+
* @default false
26+
*/
27+
disableIncrementalCache?: boolean;
28+
}
1629
interface BuildOptions {
1730
/**
1831
* Minify the server bundle.
@@ -39,6 +52,10 @@ interface BuildOptions {
3952
* });
4053
* ```
4154
*/
55+
/**
56+
* Dangerous options. This break some functionnality but can be useful in some cases.
57+
*/
58+
dangerous?: DangerousOptions;
4259
buildCommand?: string;
4360
/**
4461
* The path to the target folder of build output from the `buildCommand` option (the path which will contain the `.next` and `.open-next` folders). This path is relative from the current process.cwd().
@@ -82,8 +99,10 @@ export async function build(opts: BuildOptions = {}) {
8299
printHeader("Generating bundle");
83100
initOutputDir();
84101
createStaticAssets();
85-
createCacheAssets(monorepoRoot);
86-
await createServerBundle(monorepoRoot, opts.streaming);
102+
if (!options.dangerous?.disableIncrementalCache) {
103+
createCacheAssets(monorepoRoot, options.dangerous?.disableDynamoDBCache);
104+
}
105+
await createServerBundle(monorepoRoot, options.streaming);
87106
createRevalidationBundle();
88107
createImageOptimizationBundle();
89108
createWarmerBundle();
@@ -109,6 +128,8 @@ function normalizeOptions(opts: BuildOptions, root: string) {
109128
minify: opts.minify ?? Boolean(process.env.OPEN_NEXT_MINIFY) ?? false,
110129
debug: opts.debug ?? Boolean(process.env.OPEN_NEXT_DEBUG) ?? false,
111130
buildCommand: opts.buildCommand,
131+
dangerous: opts.dangerous,
132+
streaming: opts.streaming ?? false,
112133
};
113134
}
114135

@@ -369,7 +390,7 @@ function createStaticAssets() {
369390
}
370391
}
371392

372-
function createCacheAssets(monorepoRoot: string) {
393+
function createCacheAssets(monorepoRoot: string, disableDynamoDBCache = false) {
373394
console.info(`Bundling cache assets...`);
374395

375396
const { appBuildOutputPath, outputDir } = options;
@@ -398,23 +419,14 @@ function createCacheAssets(monorepoRoot: string) {
398419
(file.endsWith(".html") && htmlPages.has(file)),
399420
);
400421

401-
// Generate dynamodb data
402-
// We need to traverse the cache to find every .meta file
403-
const metaFiles: {
404-
tag: { S: string };
405-
path: { S: string };
406-
revalidatedAt: { N: string };
407-
}[] = [];
408-
409-
// Copy fetch-cache to cache folder
410-
const fetchCachePath = path.join(
411-
appBuildOutputPath,
412-
".next/cache/fetch-cache",
413-
);
414-
if (fs.existsSync(fetchCachePath)) {
415-
const fetchOutputPath = path.join(outputDir, "cache", "__fetch", buildId);
416-
fs.mkdirSync(fetchOutputPath, { recursive: true });
417-
fs.cpSync(fetchCachePath, fetchOutputPath, { recursive: true });
422+
if (!disableDynamoDBCache) {
423+
// Generate dynamodb data
424+
// We need to traverse the cache to find every .meta file
425+
const metaFiles: {
426+
tag: { S: string };
427+
path: { S: string };
428+
revalidatedAt: { N: string };
429+
}[] = [];
418430

419431
// Compute dynamodb cache data
420432
// Traverse files inside cache to find all meta files and cache tags associated with them
@@ -444,41 +456,54 @@ function createCacheAssets(monorepoRoot: string) {
444456
},
445457
);
446458

447-
traverseFiles(
448-
fetchCachePath,
449-
() => true,
450-
(filepath) => {
451-
const fileContent = fs.readFileSync(filepath, "utf8");
452-
const fileData = JSON.parse(fileContent);
453-
fileData?.tags?.forEach((tag: string) => {
454-
metaFiles.push({
455-
tag: { S: path.posix.join(buildId, tag) },
456-
path: {
457-
S: path.posix.join(
458-
buildId,
459-
path.relative(fetchCachePath, filepath),
460-
),
461-
},
462-
revalidatedAt: { N: `${Date.now()}` },
463-
});
464-
});
465-
},
459+
// Copy fetch-cache to cache folder
460+
const fetchCachePath = path.join(
461+
appBuildOutputPath,
462+
".next/cache/fetch-cache",
466463
);
464+
if (fs.existsSync(fetchCachePath)) {
465+
const fetchOutputPath = path.join(outputDir, "cache", "__fetch", buildId);
466+
fs.mkdirSync(fetchOutputPath, { recursive: true });
467+
fs.cpSync(fetchCachePath, fetchOutputPath, { recursive: true });
467468

468-
const providerPath = path.join(outputDir, "dynamodb-provider");
469+
traverseFiles(
470+
fetchCachePath,
471+
() => true,
472+
(filepath) => {
473+
const fileContent = fs.readFileSync(filepath, "utf8");
474+
const fileData = JSON.parse(fileContent);
475+
fileData?.tags?.forEach((tag: string) => {
476+
metaFiles.push({
477+
tag: { S: path.posix.join(buildId, tag) },
478+
path: {
479+
S: path.posix.join(
480+
buildId,
481+
path.relative(fetchCachePath, filepath),
482+
),
483+
},
484+
revalidatedAt: { N: `${Date.now()}` },
485+
});
486+
});
487+
},
488+
);
489+
}
469490

470-
esbuildSync({
471-
external: ["@aws-sdk/client-dynamodb"],
472-
entryPoints: [path.join(__dirname, "adapters", "dynamo-provider.js")],
473-
outfile: path.join(providerPath, "index.mjs"),
474-
target: ["node18"],
475-
});
491+
if (metaFiles.length > 0) {
492+
const providerPath = path.join(outputDir, "dynamodb-provider");
476493

477-
// TODO: check if metafiles doesn't contain duplicates
478-
fs.writeFileSync(
479-
path.join(providerPath, "dynamodb-cache.json"),
480-
JSON.stringify(metaFiles),
481-
);
494+
esbuildSync({
495+
external: ["@aws-sdk/client-dynamodb"],
496+
entryPoints: [path.join(__dirname, "adapters", "dynamo-provider.js")],
497+
outfile: path.join(providerPath, "index.mjs"),
498+
target: ["node18"],
499+
});
500+
501+
// TODO: check if metafiles doesn't contain duplicates
502+
fs.writeFileSync(
503+
path.join(providerPath, "dynamodb-cache.json"),
504+
JSON.stringify(metaFiles),
505+
);
506+
}
482507
}
483508
}
484509

@@ -602,7 +627,7 @@ async function createServerBundle(monorepoRoot: string, streaming = false) {
602627
addPublicFilesList(outputPath, packagePath);
603628
injectMiddlewareGeolocation(outputPath, packagePath);
604629
removeCachedPages(outputPath, packagePath);
605-
addCacheHandler(outputPath);
630+
addCacheHandler(outputPath, options.dangerous);
606631
}
607632

608633
function addMonorepoEntrypoint(outputPath: string, packagePath: string) {
@@ -710,13 +735,20 @@ function removeCachedPages(outputPath: string, packagePath: string) {
710735
);
711736
}
712737

713-
function addCacheHandler(outputPath: string) {
738+
function addCacheHandler(outputPath: string, options?: DangerousOptions) {
714739
esbuildSync({
715740
external: ["next", "styled-jsx", "react"],
716741
entryPoints: [path.join(__dirname, "adapters", "cache.js")],
717742
outfile: path.join(outputPath, "cache.cjs"),
718743
target: ["node18"],
719744
format: "cjs",
745+
banner: {
746+
js: `globalThis.disableIncrementalCache = ${
747+
options?.disableIncrementalCache ?? false
748+
}; globalThis.disableDynamoDBCache = ${
749+
options?.disableDynamoDBCache ?? false
750+
};`,
751+
},
720752
});
721753
}
722754

packages/open-next/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ build({
1414
appPath: args["--app-path"],
1515
minify: Object.keys(args).includes("--minify"),
1616
streaming: Object.keys(args).includes("--streaming"),
17+
dangerous: {
18+
disableDynamoDBCache: Object.keys(args).includes(
19+
"--dangerously-disable-dynamodb-cache",
20+
),
21+
disableIncrementalCache: Object.keys(args).includes(
22+
"--dangerously-disable-incremental-cache",
23+
),
24+
},
1725
});
1826

1927
function parseArgs() {

0 commit comments

Comments
 (0)