Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit f4a70d5

Browse files
committed
Fix unbound subrequest limit, limit internal API calls, closes #274
- Increased subrequest limit for `unbound` workers from 50 to 1000 - Limit calls to internal APIs (KV/Durable Objects) to 1000 See https://developers.cloudflare.com/workers/platform/limits#subrequests.
1 parent 0f8b7af commit f4a70d5

File tree

27 files changed

+514
-95
lines changed

27 files changed

+514
-95
lines changed

packages/cache/src/cache.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export class Cache implements CacheInterface {
165165
res: BaseResponse | Response
166166
): Promise<undefined> {
167167
if (this.#blockGlobalAsyncIO) assertInRequest();
168-
getRequestContext()?.incrementSubrequests();
168+
getRequestContext()?.incrementExternalSubrequests();
169169
req = normaliseRequest(req);
170170

171171
if (res instanceof Response && res.webSocket) {
@@ -208,7 +208,7 @@ export class Cache implements CacheInterface {
208208
options?: CacheMatchOptions
209209
): Promise<Response | undefined> {
210210
if (this.#blockGlobalAsyncIO) assertInRequest();
211-
getRequestContext()?.incrementSubrequests();
211+
getRequestContext()?.incrementExternalSubrequests();
212212
req = normaliseRequest(req);
213213
// Cloudflare only caches GET requests
214214
if (req.method !== "GET" && !options?.ignoreMethod) return;
@@ -250,7 +250,7 @@ export class Cache implements CacheInterface {
250250
options?: CacheMatchOptions
251251
): Promise<boolean> {
252252
if (this.#blockGlobalAsyncIO) assertInRequest();
253-
getRequestContext()?.incrementSubrequests();
253+
getRequestContext()?.incrementExternalSubrequests();
254254
req = normaliseRequest(req);
255255
// Cloudflare only caches GET requests
256256
if (req.method !== "GET" && !options?.ignoreMethod) return false;

packages/cache/test/cache.spec.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import assert from "assert";
22
import { URL } from "url";
33
import { Cache, CacheError, CachedMeta } from "@miniflare/cache";
44
import { Request, RequestInitCfProperties, Response } from "@miniflare/core";
5-
import { RequestContext, Storage } from "@miniflare/shared";
5+
import {
6+
EXTERNAL_SUBREQUEST_LIMIT_BUNDLED,
7+
RequestContext,
8+
RequestContextOptions,
9+
Storage,
10+
} from "@miniflare/shared";
611
import {
712
getObjectProperties,
813
utf8Decode,
@@ -21,6 +26,10 @@ import {
2126
} from "undici";
2227
import { testResponse } from "./helpers";
2328

29+
const requestCtxOptions: RequestContextOptions = {
30+
externalSubrequestLimit: EXTERNAL_SUBREQUEST_LIMIT_BUNDLED,
31+
};
32+
2433
interface Context {
2534
storage: Storage;
2635
clock: { timestamp: number };
@@ -193,9 +202,9 @@ test("Cache: put respects cf cacheTtlByStatus", async (t) => {
193202

194203
test("Cache: put increments subrequest count", async (t) => {
195204
const { cache } = t.context;
196-
const ctx = new RequestContext();
205+
const ctx = new RequestContext(requestCtxOptions);
197206
await ctx.runWith(() => cache.put("http://localhost:8787/", testResponse()));
198-
t.is(ctx.subrequests, 1);
207+
t.is(ctx.externalSubrequests, 1);
199208
});
200209
test("Cache: put waits for output gate to open before storing", (t) => {
201210
const { cache } = t.context;
@@ -251,9 +260,9 @@ test("Cache: only matches non-GET requests when ignoring method", async (t) => {
251260

252261
test("Cache: match increments subrequest count", async (t) => {
253262
const { cache } = t.context;
254-
const ctx = new RequestContext();
263+
const ctx = new RequestContext(requestCtxOptions);
255264
await ctx.runWith(() => cache.match("http://localhost:8787/"));
256-
t.is(ctx.subrequests, 1);
265+
t.is(ctx.externalSubrequests, 1);
257266
});
258267
test("Cache: match MISS waits for input gate to open before returning", async (t) => {
259268
const { cache } = t.context;
@@ -311,9 +320,9 @@ test("Cache: only deletes non-GET requests when ignoring method", async (t) => {
311320

312321
test("Cache: delete increments subrequest count", async (t) => {
313322
const { cache } = t.context;
314-
const ctx = new RequestContext();
323+
const ctx = new RequestContext(requestCtxOptions);
315324
await ctx.runWith(() => cache.delete("http://localhost:8787/"));
316-
t.is(ctx.subrequests, 1);
325+
t.is(ctx.externalSubrequests, 1);
317326
});
318327
test("Cache: delete waits for output gate to open before deleting", async (t) => {
319328
const { cache } = t.context;
@@ -413,7 +422,7 @@ test("Cache: hides implementation details", (t) => {
413422
});
414423
test("Cache: operations throw outside request handler", async (t) => {
415424
const cache = new Cache(new MemoryStorage(), { blockGlobalAsyncIO: true });
416-
const ctx = new RequestContext();
425+
const ctx = new RequestContext(requestCtxOptions);
417426

418427
const expectations: ThrowsExpectation = {
419428
instanceOf: Error,

packages/cli-parser/test/help.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Core Options:
4141
from
4242
--compat-flag Control specific backwards-incompatible [array]
4343
changes
44+
--usage-model Usage model (bundled by default) [string]
4445
-u, --upstream URL of upstream origin [string]
4546
-w, --watch Watch files for changes [boolean]
4647
-d, --debug Enable debug logging [boolean]

packages/core/src/index.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ import {
2121
SetupResult,
2222
StorageFactory,
2323
TypedEventTarget,
24+
UsageModel,
2425
WranglerConfig,
2526
addAll,
2627
logOptions,
2728
resolveStoragePersist,
29+
usageModelExternalSubrequestLimit,
2830
} from "@miniflare/shared";
2931
import type { Watcher } from "@miniflare/watcher";
3032
import { dequal } from "dequal/lite";
@@ -251,6 +253,7 @@ export class MiniflareCore<
251253

252254
#compat?: Compatibility;
253255
#previousRootPath?: string;
256+
#previousUsageModel?: UsageModel;
254257
#previousGlobalAsyncIO?: boolean;
255258
#instances?: PluginInstances<Plugins>;
256259
#mounts?: Map<string, MiniflareCore<Plugins>>;
@@ -373,10 +376,11 @@ export class MiniflareCore<
373376

374377
// Build compatibility manager, rebuild all plugins if reloadAll is set,
375378
// compatibility data, root path or any limits have changed
376-
const { compatibilityDate, compatibilityFlags, globalAsyncIO } =
379+
const { compatibilityDate, compatibilityFlags, usageModel, globalAsyncIO } =
377380
options.CorePlugin;
378381
let ctxUpdate =
379382
(this.#previousRootPath && this.#previousRootPath !== rootPath) ||
383+
this.#previousUsageModel !== usageModel ||
380384
this.#previousGlobalAsyncIO !== globalAsyncIO ||
381385
reloadAll;
382386
this.#previousRootPath = rootPath;
@@ -392,6 +396,7 @@ export class MiniflareCore<
392396
log: this.#ctx.log,
393397
compat: this.#compat,
394398
rootPath,
399+
usageModel,
395400
globalAsyncIO,
396401
};
397402

@@ -791,13 +796,15 @@ export class MiniflareCore<
791796
// `Mount`'s `dispatchFetch` requires a function with signature
792797
// `(Request) => Awaitable<Response>` too
793798
dispatchFetch: (request) => this[kDispatchFetch](request, true),
799+
usageModel: this.#instances!.CorePlugin.usageModel,
794800
} as _CoreMount);
795801
}
796802
// Add all other mounts
797803
for (const [name, mount] of this.#mounts!) {
798804
mounts.set(name, {
799805
moduleExports: await mount.getModuleExports(),
800806
dispatchFetch: (request) => mount[kDispatchFetch](request, true),
807+
usageModel: mount.#instances!.CorePlugin.usageModel,
801808
} as _CoreMount);
802809
}
803810
await this.#runAllReloads(mounts);
@@ -1026,7 +1033,7 @@ export class MiniflareCore<
10261033

10271034
// If upstream set, and the request URL doesn't begin with it, rewrite it
10281035
// so fetching the incoming request gets a response from the upstream
1029-
const { upstreamURL } = this.#instances!.CorePlugin;
1036+
const { upstreamURL, usageModel } = this.#instances!.CorePlugin;
10301037
if (upstreamURL && !url.toString().startsWith(upstreamURL.toString())) {
10311038
let path = url.pathname + url.search;
10321039
// Remove leading slash so we resolve relative to upstream's path
@@ -1046,6 +1053,7 @@ export class MiniflareCore<
10461053
return new RequestContext({
10471054
requestDepth,
10481055
pipelineDepth: 1,
1056+
externalSubrequestLimit: usageModelExternalSubrequestLimit(usageModel),
10491057
}).runWith(() =>
10501058
this[kDispatchFetch](
10511059
request,
@@ -1088,10 +1096,13 @@ export class MiniflareCore<
10881096
if (mount) return mount.dispatchScheduled(scheduledTime, cron);
10891097
}
10901098

1099+
const { usageModel } = this.#instances!.CorePlugin;
10911100
const globalScope = this.#globalScope;
10921101
// Each fetch gets its own context (e.g. 50 subrequests).
10931102
// Start a new pipeline too.
1094-
return new RequestContext().runWith(() =>
1103+
return new RequestContext({
1104+
externalSubrequestLimit: usageModelExternalSubrequestLimit(usageModel),
1105+
}).runWith(() =>
10951106
globalScope![kDispatchScheduled]<WaitUntil>(scheduledTime, cron)
10961107
);
10971108
}

packages/core/src/plugins/bindings.ts

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import {
1111
PluginContext,
1212
RequestContext,
1313
SetupResult,
14+
UsageModel,
1415
WranglerServiceConfig,
1516
getRequestContext,
17+
usageModelExternalSubrequestLimit,
1618
viewToBuffer,
1719
} from "@miniflare/shared";
1820
import dotenv from "dotenv";
@@ -28,6 +30,14 @@ export type _CoreMount = Mount<Request, Response>; // yuck :(
2830
// some other custom way (e.g. Cloudflare Pages' `env.PAGES` asset handler)
2931
export type FetcherFetch = (request: Request) => Awaitable<Response>;
3032

33+
export interface FetcherFetchWithUsageModel {
34+
fetch: FetcherFetch;
35+
// Usage model required as mount might have different usage model,
36+
// and therefore different subrequest limits.
37+
// We need to know these when creating the request context.
38+
usageModel?: UsageModel;
39+
}
40+
3141
export type ServiceBindingsOptions = Record<
3242
string,
3343
| string // Just service name, environment defaults to "production"
@@ -54,34 +64,48 @@ export interface BindingsOptions {
5464

5565
export class Fetcher {
5666
readonly #service: string | FetcherFetch;
57-
readonly #getServiceFetch: (name: string) => Promise<FetcherFetch>;
67+
readonly #getServiceFetch: (
68+
name: string
69+
) => Promise<FetcherFetchWithUsageModel>;
70+
readonly #defaultUsageModel?: UsageModel;
5871

5972
constructor(
6073
service: string | FetcherFetch,
61-
getServiceFetch: (name: string) => Promise<FetcherFetch>
74+
getServiceFetch: (name: string) => Promise<FetcherFetchWithUsageModel>,
75+
defaultUsageModel?: UsageModel
6276
) {
6377
this.#service = service;
6478
this.#getServiceFetch = getServiceFetch;
79+
this.#defaultUsageModel = defaultUsageModel;
6580
}
6681

6782
async fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {
83+
// Always create new Request instance, so clean object passed to services
84+
const req = new Request(input, init);
85+
86+
// If we're using a custom fetch handler, call that or wait for the service
87+
// fetch handler to be available
88+
let fetch: FetcherFetch;
89+
let usageModel = this.#defaultUsageModel;
90+
if (typeof this.#service === "function") {
91+
fetch = this.#service;
92+
} else {
93+
const serviceFetch = await this.#getServiceFetch(this.#service);
94+
fetch = serviceFetch.fetch;
95+
usageModel = serviceFetch.usageModel;
96+
}
97+
6898
// Check we're not too deep, should throw in the caller and NOT return a
6999
// 500 Internal Server Error Response from this function
70100
const parentCtx = getRequestContext();
71101
const requestDepth = parentCtx?.requestDepth ?? 1;
72102
const pipelineDepth = (parentCtx?.pipelineDepth ?? 0) + 1;
73103
// NOTE: `new RequestContext` throws if too deep
74-
const ctx = new RequestContext({ requestDepth, pipelineDepth });
75-
76-
// Always create new Request instance, so clean object passed to services
77-
const req = new Request(input, init);
78-
79-
// If we're using a custom fetch handler, call that or wait for the service
80-
// fetch handler to be available
81-
const fetch =
82-
typeof this.#service === "function"
83-
? this.#service
84-
: await this.#getServiceFetch(this.#service);
104+
const ctx = new RequestContext({
105+
requestDepth,
106+
pipelineDepth,
107+
externalSubrequestLimit: usageModelExternalSubrequestLimit(usageModel),
108+
});
85109

86110
// Cloudflare Workers currently don't propagate errors thrown by the service
87111
// when handling the request. Instead a 500 Internal Server Error Response
@@ -256,7 +280,9 @@ export class BindingsPlugin
256280
}
257281
}
258282

259-
#getServiceFetch = async (service: string): Promise<FetcherFetch> => {
283+
#getServiceFetch = async (
284+
service: string
285+
): Promise<FetcherFetchWithUsageModel> => {
260286
// Wait for mounts
261287
assert(
262288
this.#contextPromise,
@@ -266,9 +292,9 @@ export class BindingsPlugin
266292

267293
// Should've thrown error earlier in reload if service not found and
268294
// dispatchFetch should always be set, it's optional to make testing easier.
269-
const fetch = this.#mounts?.get(service)?.dispatchFetch;
270-
assert(fetch);
271-
return fetch;
295+
const mount = this.#mounts?.get(service);
296+
assert(mount?.dispatchFetch);
297+
return { fetch: mount.dispatchFetch, usageModel: mount.usageModel };
272298
};
273299

274300
async setup(): Promise<SetupResult> {
@@ -336,7 +362,11 @@ export class BindingsPlugin
336362

337363
// 6) Load service bindings
338364
for (const { name, service } of this.#processedServiceBindings) {
339-
bindings[name] = new Fetcher(service, this.#getServiceFetch);
365+
bindings[name] = new Fetcher(
366+
service,
367+
this.#getServiceFetch,
368+
this.ctx.usageModel
369+
);
340370
}
341371

342372
// 7) Copy user's arbitrary bindings

packages/core/src/plugins/core.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export interface CoreOptions {
9090
modulesRules?: ModuleRule[];
9191
compatibilityDate?: string;
9292
compatibilityFlags?: CompatibilityFlag[];
93+
usageModel?: "bundled" | "unbound";
9394
upstream?: string;
9495
watch?: boolean;
9596
// CLI only options, not actually used by MiniflareCore
@@ -231,6 +232,14 @@ export class CorePlugin extends Plugin<CoreOptions> implements CoreOptions {
231232
})
232233
compatibilityFlags?: CompatibilityFlag[];
233234

235+
@Option({
236+
type: OptionType.STRING,
237+
name: "usage-model",
238+
description: "Usage model (bundled by default)",
239+
fromWrangler: ({ usage_model }) => usage_model,
240+
})
241+
usageModel?: "bundled" | "unbound";
242+
234243
@Option({
235244
type: OptionType.STRING,
236245
alias: "u",

packages/core/src/standards/http.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,7 @@ export async function fetch(
769769
// https://developers.cloudflare.com/workers/examples/cache-using-fetch
770770

771771
const ctx = getRequestContext();
772-
ctx?.incrementSubrequests();
772+
ctx?.incrementExternalSubrequests();
773773

774774
await waitForOpenOutputGate();
775775

@@ -819,7 +819,7 @@ export async function fetch(
819819
if (baseRes.redirected && ctx) {
820820
const urlList = _getURLList(baseRes);
821821
// Last url is final destination, so subtract 1 for redirect count
822-
if (urlList) ctx.incrementSubrequests(urlList.length - 1);
822+
if (urlList) ctx.incrementExternalSubrequests(urlList.length - 1);
823823
}
824824

825825
// Convert the response to our hybrid Response

packages/core/test/index.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,7 +1161,7 @@ test("MiniflareCore: dispatchFetch: creates new request context", async (t) => {
11611161
{
11621162
globals: {
11631163
assertSubrequests(expected: number) {
1164-
t.is(getRequestContext()?.subrequests, expected);
1164+
t.is(getRequestContext()?.externalSubrequests, expected);
11651165
},
11661166
},
11671167
},
@@ -1266,7 +1266,7 @@ test("MiniflareCore: dispatchScheduled: creates new request context", async (t)
12661266
{
12671267
globals: {
12681268
assertSubrequests(expected: number) {
1269-
t.is(getRequestContext()?.subrequests, expected);
1269+
t.is(getRequestContext()?.externalSubrequests, expected);
12701270
},
12711271
},
12721272
modules: true,

0 commit comments

Comments
 (0)