Skip to content

Commit 8721156

Browse files
committed
regional replication
1 parent 6821828 commit 8721156

File tree

3 files changed

+169
-8
lines changed

3 files changed

+169
-8
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default defineCloudflareConfig({
1111
shardReplication: {
1212
numberOfSoftReplicas: 8,
1313
numberOfHardReplicas: 2,
14+
enableRegionalReplication: true,
1415
},
1516
}),
1617
queue: doQueue,

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

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

3-
import shardedDOTagCache, { DOId } from "./do-sharded-tag-cache";
3+
import shardedDOTagCache, { AVAILABLE_REGIONS, DOId } from "./do-sharded-tag-cache";
4+
45

56
const hasBeenRevalidatedMock = vi.fn();
67
const writeTagsMock = vi.fn();
@@ -9,6 +10,8 @@ const getMock = vi
910
.fn()
1011
.mockReturnValue({ hasBeenRevalidated: hasBeenRevalidatedMock, writeTags: writeTagsMock });
1112
const waitUntilMock = vi.fn().mockImplementation(async (fn) => fn());
13+
// @ts-expect-error - We define it here only for the test
14+
globalThis.continent = undefined;
1215
const sendDLQMock = vi.fn();
1316
vi.mock("../../cloudflare-context", () => ({
1417
getCloudflareContext: () => ({
@@ -19,6 +22,10 @@ vi.mock("../../cloudflare-context", () => ({
1922
},
2023
},
2124
ctx: { waitUntil: waitUntilMock },
25+
cf: {
26+
// @ts-expect-error - We define it here only for the test
27+
continent: globalThis.continent,
28+
},
2229
}),
2330
}));
2431

@@ -108,6 +115,81 @@ describe("DOShardedTagCache", () => {
108115
expect(secondDOId?.replicaId).toBeGreaterThanOrEqual(1);
109116
expect(secondDOId?.replicaId).toBeLessThanOrEqual(2);
110117
});
118+
119+
it("should generate one doIds, but in the default region", () => {
120+
const cache = shardedDOTagCache({
121+
baseShardSize: 4,
122+
shardReplication: {
123+
numberOfSoftReplicas: 2,
124+
numberOfHardReplicas: 2,
125+
enableRegionalReplication: true,
126+
},
127+
});
128+
const shardedTagCollection = cache.groupTagsByDO({
129+
tags: ["tag1", "_N_T_/tag1"],
130+
generateAllReplicas: false,
131+
});
132+
expect(shardedTagCollection.length).toBe(2);
133+
const firstDOId = shardedTagCollection[0]?.doId;
134+
const secondDOId = shardedTagCollection[1]?.doId;
135+
136+
expect(firstDOId?.shardId).toBe("tag-soft;shard-3");
137+
expect(firstDOId?.region).toBe("enam");
138+
expect(secondDOId?.shardId).toBe("tag-hard;shard-1");
139+
expect(secondDOId?.region).toBe("enam");
140+
141+
// We still need to check if the last part is between the correct boundaries
142+
expect(firstDOId?.replicaId).toBeGreaterThanOrEqual(1);
143+
expect(firstDOId?.replicaId).toBeLessThanOrEqual(2);
144+
145+
expect(secondDOId?.replicaId).toBeGreaterThanOrEqual(1);
146+
expect(secondDOId?.replicaId).toBeLessThanOrEqual(2);
147+
});
148+
149+
it("should generate one doIds, but in the correct region", () => {
150+
// @ts-expect-error - We define it here only for the test
151+
globalThis.continent = "EU";
152+
const cache = shardedDOTagCache({
153+
baseShardSize: 4,
154+
shardReplication: {
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 = shardedDOTagCache({
174+
baseShardSize: 4,
175+
shardReplication: {
176+
numberOfSoftReplicas: 2,
177+
numberOfHardReplicas: 2,
178+
enableRegionalReplication: true,
179+
},
180+
});
181+
const shardedTagCollection = cache.groupTagsByDO({
182+
tags: ["tag1", "_N_T_/tag1"],
183+
generateAllReplicas: true,
184+
});
185+
// 6 regions times 4 shards replica
186+
expect(shardedTagCollection.length).toBe(24);
187+
shardedTagCollection.forEach(({ doId }) => {
188+
expect(AVAILABLE_REGIONS).toContain(doId.region);
189+
// It should end with the region
190+
expect(doId.key).toMatch(/tag-(soft|hard);shard-\d;replica-\d;region-(enam|weur|sam|afr|apac|oc)$/);
191+
});
192+
});
111193
});
112194
});
113195

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

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export const DEFAULT_NUM_SHARDS = 4;
1212
export const NAME = "do-sharded-tag-cache";
1313

1414
const SOFT_TAG_PREFIX = "_N_T_/";
15+
export const DEFAULT_REGION = "enam" as const;
16+
export const AVAILABLE_REGIONS = ["enam", "weur", "apac", "sam", "afr", "oc"] as const;
17+
type AllowedDurableObjectRegion = (typeof AVAILABLE_REGIONS)[number];
1518

1619
interface ShardedDOTagCacheOptions {
1720
/**
@@ -63,6 +66,21 @@ interface ShardedDOTagCacheOptions {
6366
shardReplication?: {
6467
numberOfSoftReplicas: number;
6568
numberOfHardReplicas: number;
69+
70+
/**
71+
* Whether to enable regional replication
72+
* Regional replication will duplicate each shards and their associated replicas into every regions
73+
* This will reduce the latency for the read operations
74+
* On write, the write will be sent to all the shards and all the replicas in all the regions
75+
* @default false
76+
*/
77+
enableRegionalReplication?: boolean;
78+
79+
/**
80+
* Default region to use for the regional replication when the region cannot be determined
81+
* @default "enam"
82+
*/
83+
defaultRegion?: AllowedDurableObjectRegion;
6684
};
6785

6886
/**
@@ -78,23 +96,26 @@ interface DOIdOptions {
7896
numberOfReplicas: number;
7997
shardType: "soft" | "hard";
8098
replicaId?: number;
99+
region?: DurableObjectLocationHint;
81100
}
82101

83102
export class DOId {
84103
shardId: string;
85104
replicaId: number;
105+
region?: DurableObjectLocationHint;
86106
constructor(public options: DOIdOptions) {
87-
const { baseShardId, shardType, numberOfReplicas, replicaId } = options;
107+
const { baseShardId, shardType, numberOfReplicas, replicaId, region } = options;
88108
this.shardId = `tag-${shardType};${baseShardId}`;
89109
this.replicaId = replicaId ?? this.generateRandomNumberBetween(1, numberOfReplicas);
110+
this.region = region;
90111
}
91112

92113
private generateRandomNumberBetween(min: number, max: number) {
93114
return Math.floor(Math.random() * (max - min + 1) + min);
94115
}
95116

96117
get key() {
97-
return `${this.shardId};replica-${this.replicaId}`;
118+
return `${this.shardId};replica-${this.replicaId}${this.region ? `;region-${this.region}` : ""}`;
98119
}
99120
}
100121

@@ -109,20 +130,28 @@ class ShardedDOTagCache implements NextModeTagCache {
109130
readonly numSoftReplicas: number;
110131
readonly numHardReplicas: number;
111132
readonly maxWriteRetries: number;
133+
readonly enableRegionalReplication: boolean;
134+
readonly defaultRegion: AllowedDurableObjectRegion;
112135
localCache?: Cache;
113136

114137
constructor(private opts: ShardedDOTagCacheOptions = { baseShardSize: DEFAULT_NUM_SHARDS }) {
115138
this.numSoftReplicas = opts.shardReplication?.numberOfSoftReplicas ?? 1;
116139
this.numHardReplicas = opts.shardReplication?.numberOfHardReplicas ?? 1;
117140
this.maxWriteRetries = opts.maxWriteRetries ?? DEFAULT_WRITE_RETRIES;
141+
this.enableRegionalReplication = opts.shardReplication?.enableRegionalReplication ?? false;
142+
this.defaultRegion = opts.shardReplication?.defaultRegion ?? DEFAULT_REGION;
118143
}
119144

120145
private getDurableObjectStub(doId: DOId) {
121146
const durableObject = getCloudflareContext().env.NEXT_TAG_CACHE_DO_SHARDED;
122147
if (!durableObject) throw new IgnorableError("No durable object binding for cache revalidation");
123148

124149
const id = durableObject.idFromName(doId.key);
125-
return durableObject.get(id);
150+
debug("[shardedTagCache] - Accessing Durable Object : ", {
151+
key: doId.key,
152+
region: doId.region,
153+
});
154+
return durableObject.get(id, { locationHint: doId.region });
126155
}
127156

128157
/**
@@ -143,10 +172,14 @@ class ShardedDOTagCache implements NextModeTagCache {
143172
}) {
144173
let replicaIndexes: Array<number | undefined> = [1];
145174
const isSoft = shardType === "soft";
146-
const numReplicas = isSoft ? this.numSoftReplicas : this.numHardReplicas;
147-
replicaIndexes = generateAllReplicas ? Array.from({ length: numReplicas }, (_, i) => i + 1) : [undefined];
148-
149-
return replicaIndexes.flatMap((replicaId) => {
175+
let numReplicas = 1;
176+
if (this.opts.shardReplication) {
177+
numReplicas = isSoft ? this.numSoftReplicas : this.numHardReplicas;
178+
replicaIndexes = generateAllReplicas
179+
? Array.from({ length: numReplicas }, (_, i) => i + 1)
180+
: [undefined];
181+
}
182+
const regionalReplicas = replicaIndexes.flatMap((replicaId) => {
150183
return tags
151184
.filter((tag) => (isSoft ? tag.startsWith(SOFT_TAG_PREFIX) : !tag.startsWith(SOFT_TAG_PREFIX)))
152185
.map((tag) => {
@@ -161,6 +194,51 @@ class ShardedDOTagCache implements NextModeTagCache {
161194
};
162195
});
163196
});
197+
if (!this.enableRegionalReplication) return regionalReplicas;
198+
199+
// If we have regional replication enabled, we need to further duplicate the shards in all the regions
200+
const regionalReplicasInAllRegions = generateAllReplicas
201+
? regionalReplicas.flatMap(({ doId, tag }) => {
202+
return AVAILABLE_REGIONS.map((region) => {
203+
return {
204+
doId: new DOId({
205+
baseShardId: doId.options.baseShardId,
206+
numberOfReplicas: numReplicas,
207+
shardType,
208+
replicaId: doId.replicaId,
209+
region,
210+
}),
211+
tag,
212+
};
213+
});
214+
})
215+
: regionalReplicas.map(({ doId, tag }) => {
216+
doId.region = this.getClosestRegion();
217+
return { doId, tag };
218+
});
219+
return regionalReplicasInAllRegions;
220+
}
221+
222+
getClosestRegion() {
223+
const continent = getCloudflareContext().cf?.continent;
224+
if (!continent) return this.defaultRegion;
225+
debug("[shardedTagCache] - Continent : ", continent);
226+
switch (continent) {
227+
case "AF":
228+
return "afr";
229+
case "AS":
230+
return "apac";
231+
case "EU":
232+
return "weur";
233+
case "NA":
234+
return "enam";
235+
case "OC":
236+
return "oc";
237+
case "SA":
238+
return "sam";
239+
default:
240+
return this.defaultRegion;
241+
}
164242
}
165243

166244
/**

0 commit comments

Comments
 (0)