Skip to content

Commit 4ae0dbb

Browse files
committed
regional replication
1 parent cef5e03 commit 4ae0dbb

File tree

3 files changed

+166
-6
lines changed

3 files changed

+166
-6
lines changed

examples/e2e/app-router/open-next.config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";
55

66
export default defineCloudflareConfig({
77
incrementalCache: kvIncrementalCache,
8-
// With such a configuration, we could have up to 12 * (8 + 2) = 120 Durable Objects instances
8+
// With such a configuration, we could have up to 6(all regions) * (2 * 2 * (8 + 2)) = 240 Durable Objects instances
99
tagCache: shardedTagCache({
10-
baseShardSize: 12,
10+
baseShardSize: 2,
1111
enableShardReplication: true,
1212
shardReplicationOptions: {
1313
numberOfSoftReplicas: 8,
1414
numberOfHardReplicas: 2,
15+
enableRegionalReplication: true,
1516
},
1617
}),
1718
queue: doQueue,

packages/cloudflare/src/api/overrides/tag-cache/do-sharded-tag-cache.spec.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22

33
import doShardedTagCache, {
4+
AVAILABLE_REGIONS,
45
DEFAULT_HARD_REPLICAS,
56
DEFAULT_SOFT_REPLICAS,
67
TagCacheDOId,
@@ -13,6 +14,8 @@ const getMock = vi
1314
.fn()
1415
.mockReturnValue({ hasBeenRevalidated: hasBeenRevalidatedMock, writeTags: writeTagsMock });
1516
const waitUntilMock = vi.fn().mockImplementation(async (fn) => fn());
17+
// @ts-expect-error - We define it here only for the test
18+
globalThis.continent = undefined;
1619
const sendDLQMock = vi.fn();
1720
vi.mock("../../cloudflare-context", () => ({
1821
getCloudflareContext: () => ({
@@ -23,6 +26,10 @@ vi.mock("../../cloudflare-context", () => ({
2326
},
2427
},
2528
ctx: { waitUntil: waitUntilMock },
29+
cf: {
30+
// @ts-expect-error - We define it here only for the test
31+
continent: globalThis.continent,
32+
},
2633
}),
2734
}));
2835

@@ -106,6 +113,84 @@ describe("DOShardedTagCache", () => {
106113
expect(secondDOId?.replicaId).toBeGreaterThanOrEqual(1);
107114
expect(secondDOId?.replicaId).toBeLessThanOrEqual(DEFAULT_HARD_REPLICAS);
108115
});
116+
117+
it("should generate one doIds, but in the default region", () => {
118+
const cache = doShardedTagCache({
119+
baseShardSize: 4,
120+
enableShardReplication: true,
121+
shardReplicationOptions: {
122+
numberOfSoftReplicas: 2,
123+
numberOfHardReplicas: 2,
124+
enableRegionalReplication: true,
125+
},
126+
});
127+
const shardedTagCollection = cache.groupTagsByDO({
128+
tags: ["tag1", "_N_T_/tag1"],
129+
generateAllReplicas: false,
130+
});
131+
expect(shardedTagCollection.length).toBe(2);
132+
const firstDOId = shardedTagCollection[0]?.doId;
133+
const secondDOId = shardedTagCollection[1]?.doId;
134+
135+
expect(firstDOId?.shardId).toBe("tag-soft;shard-3");
136+
expect(firstDOId?.region).toBe("enam");
137+
expect(secondDOId?.shardId).toBe("tag-hard;shard-1");
138+
expect(secondDOId?.region).toBe("enam");
139+
140+
// We still need to check if the last part is between the correct boundaries
141+
expect(firstDOId?.replicaId).toBeGreaterThanOrEqual(1);
142+
expect(firstDOId?.replicaId).toBeLessThanOrEqual(DEFAULT_SOFT_REPLICAS);
143+
144+
expect(secondDOId?.replicaId).toBeGreaterThanOrEqual(1);
145+
expect(secondDOId?.replicaId).toBeLessThanOrEqual(DEFAULT_HARD_REPLICAS);
146+
});
147+
148+
it("should generate one doIds, but in the correct region", () => {
149+
// @ts-expect-error - We define it here only for the test
150+
globalThis.continent = "EU";
151+
const cache = doShardedTagCache({
152+
baseShardSize: 4,
153+
enableShardReplication: true,
154+
shardReplicationOptions: {
155+
numberOfSoftReplicas: 2,
156+
numberOfHardReplicas: 2,
157+
enableRegionalReplication: true,
158+
},
159+
});
160+
const shardedTagCollection = cache.groupTagsByDO({
161+
tags: ["tag1", "_N_T_/tag1"],
162+
generateAllReplicas: false,
163+
});
164+
expect(shardedTagCollection.length).toBe(2);
165+
expect(shardedTagCollection[0]?.doId.region).toBe("weur");
166+
expect(shardedTagCollection[1]?.doId.region).toBe("weur");
167+
168+
//@ts-expect-error - We need to reset the global variable
169+
globalThis.continent = undefined;
170+
});
171+
172+
it("should generate all the appropriate replicas in all the regions with enableRegionalReplication", () => {
173+
const cache = doShardedTagCache({
174+
baseShardSize: 4,
175+
enableShardReplication: true,
176+
shardReplicationOptions: {
177+
numberOfSoftReplicas: 2,
178+
numberOfHardReplicas: 2,
179+
enableRegionalReplication: true,
180+
},
181+
});
182+
const shardedTagCollection = cache.groupTagsByDO({
183+
tags: ["tag1", "_N_T_/tag1"],
184+
generateAllReplicas: true,
185+
});
186+
// 6 regions times 4 shards replica
187+
expect(shardedTagCollection.length).toBe(24);
188+
shardedTagCollection.forEach(({ doId }) => {
189+
expect(AVAILABLE_REGIONS).toContain(doId.region);
190+
// It should end with the region
191+
expect(doId.key).toMatch(/tag-(soft|hard);shard-\d;replica-\d;region-(enam|weur|sam|afr|apac|oc)$/);
192+
});
193+
});
109194
});
110195
});
111196

packages/cloudflare/src/api/overrides/tag-cache/do-sharded-tag-cache.ts

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export const DEFAULT_SOFT_REPLICAS = 4;
1111
export const DEFAULT_HARD_REPLICAS = 2;
1212
export const DEFAULT_WRITE_RETRIES = 3;
1313
export const DEFAULT_NUM_SHARDS = 4;
14+
export const DEFAULT_REGION = "enam" as const;
15+
export const AVAILABLE_REGIONS = ["enam", "weur", "apac", "sam", "afr", "oc"] as const;
16+
type AllowedDurableObjectRegion = (typeof AVAILABLE_REGIONS)[number];
1417

1518
interface ShardedDOTagCacheOptions {
1619
/**
@@ -55,6 +58,21 @@ interface ShardedDOTagCacheOptions {
5558
shardReplicationOptions?: {
5659
numberOfSoftReplicas: number;
5760
numberOfHardReplicas: number;
61+
62+
/**
63+
* Whether to enable regional replication
64+
* Regional replication will duplicate each shards and their associated replicas into every regions
65+
* This will reduce the latency for the read operations
66+
* On write, the write will be sent to all the shards and all the replicas in all the regions
67+
* @default false
68+
*/
69+
enableRegionalReplication?: boolean;
70+
71+
/**
72+
* Default region to use for the regional replication when the region cannot be determined
73+
* @default "enam"
74+
*/
75+
defaultRegion?: AllowedDurableObjectRegion;
5876
};
5977

6078
/**
@@ -69,22 +87,25 @@ interface TagCacheDOIdOptions {
6987
numberOfReplicas: number;
7088
shardType: "soft" | "hard";
7189
replicaId?: number;
90+
region?: DurableObjectLocationHint;
7291
}
7392
export class TagCacheDOId {
7493
shardId: string;
7594
replicaId: number;
95+
region?: DurableObjectLocationHint;
7696
constructor(public options: TagCacheDOIdOptions) {
77-
const { baseShardId, shardType, numberOfReplicas, replicaId } = options;
97+
const { baseShardId, shardType, numberOfReplicas, replicaId, region } = options;
7898
this.shardId = `tag-${shardType};${baseShardId}`;
7999
this.replicaId = replicaId ?? this.generateRandomNumberBetween(1, numberOfReplicas);
100+
this.region = region;
80101
}
81102

82103
private generateRandomNumberBetween(min: number, max: number) {
83104
return Math.floor(Math.random() * (max - min + 1) + min);
84105
}
85106

86107
get key() {
87-
return `${this.shardId};replica-${this.replicaId}`;
108+
return `${this.shardId};replica-${this.replicaId}${this.region ? `;region-${this.region}` : ""}`;
88109
}
89110
}
90111
class ShardedDOTagCache implements NextModeTagCache {
@@ -93,20 +114,28 @@ class ShardedDOTagCache implements NextModeTagCache {
93114
readonly numSoftReplicas: number;
94115
readonly numHardReplicas: number;
95116
readonly maxWriteRetries: number;
117+
readonly enableRegionalReplication: boolean;
118+
readonly defaultRegion: AllowedDurableObjectRegion;
96119
localCache?: Cache;
97120

98121
constructor(private opts: ShardedDOTagCacheOptions = { baseShardSize: DEFAULT_NUM_SHARDS }) {
99122
this.numSoftReplicas = opts.shardReplicationOptions?.numberOfSoftReplicas ?? DEFAULT_SOFT_REPLICAS;
100123
this.numHardReplicas = opts.shardReplicationOptions?.numberOfHardReplicas ?? DEFAULT_HARD_REPLICAS;
101124
this.maxWriteRetries = opts.maxWriteRetries ?? DEFAULT_WRITE_RETRIES;
125+
this.enableRegionalReplication = opts.shardReplicationOptions?.enableRegionalReplication ?? false;
126+
this.defaultRegion = opts.shardReplicationOptions?.defaultRegion ?? DEFAULT_REGION;
102127
}
103128

104129
private getDurableObjectStub(doId: TagCacheDOId) {
105130
const durableObject = getCloudflareContext().env.NEXT_TAG_CACHE_DO_SHARDED;
106131
if (!durableObject) throw new IgnorableError("No durable object binding for cache revalidation");
107132

108133
const id = durableObject.idFromName(doId.key);
109-
return durableObject.get(id);
134+
debug("[shardedTagCache] - Accessing Durable Object : ", {
135+
key: doId.key,
136+
region: doId.region,
137+
});
138+
return durableObject.get(id, { locationHint: doId.region });
110139
}
111140

112141
/**
@@ -134,7 +163,7 @@ class ShardedDOTagCache implements NextModeTagCache {
134163
? Array.from({ length: numReplicas }, (_, i) => i + 1)
135164
: [undefined];
136165
}
137-
return replicaIndexes.flatMap((replicaId) => {
166+
const regionalReplicas = replicaIndexes.flatMap((replicaId) => {
138167
return tags
139168
.filter((tag) => (isSoft ? tag.startsWith(SOFT_TAG_PREFIX) : !tag.startsWith(SOFT_TAG_PREFIX)))
140169
.map((tag) => {
@@ -149,6 +178,51 @@ class ShardedDOTagCache implements NextModeTagCache {
149178
};
150179
});
151180
});
181+
if (!this.enableRegionalReplication) return regionalReplicas;
182+
183+
// If we have regional replication enabled, we need to further duplicate the shards in all the regions
184+
const regionalReplicasInAllRegions = generateAllReplicas
185+
? regionalReplicas.flatMap(({ doId, tag }) => {
186+
return AVAILABLE_REGIONS.map((region) => {
187+
return {
188+
doId: new TagCacheDOId({
189+
baseShardId: doId.options.baseShardId,
190+
numberOfReplicas: numReplicas,
191+
shardType,
192+
replicaId: doId.replicaId,
193+
region,
194+
}),
195+
tag,
196+
};
197+
});
198+
})
199+
: regionalReplicas.map(({ doId, tag }) => {
200+
doId.region = this.getClosestRegion();
201+
return { doId, tag };
202+
});
203+
return regionalReplicasInAllRegions;
204+
}
205+
206+
getClosestRegion() {
207+
const continent = getCloudflareContext().cf?.continent;
208+
if (!continent) return this.defaultRegion;
209+
debug("[shardedTagCache] - Continent : ", continent);
210+
switch (continent) {
211+
case "AF":
212+
return "afr";
213+
case "AS":
214+
return "apac";
215+
case "EU":
216+
return "weur";
217+
case "NA":
218+
return "enam";
219+
case "OC":
220+
return "oc";
221+
case "SA":
222+
return "sam";
223+
default:
224+
return this.defaultRegion;
225+
}
152226
}
153227

154228
/**

0 commit comments

Comments
 (0)