Apps on g1t.page answer each request within 50 ms of CPU time and 50 requests of their own
4 files+53−40/4 viewed
| 124 | 124 | are served under the binding name your config gives them. Cron triggers | |
| 125 | 125 | (`triggers.crons`) are not scheduled, so a `scheduled` handler never | |
| 126 | 126 | runs; the deployment says so in its warnings. | |
| 127 | + | - Each request your Worker answers may use up to 50 ms of CPU time | |
| 128 | + | (waiting on the network does not count) and make up to 50 requests of | |
| 129 | + | its own (`fetch` calls and the like). A request that goes over either | |
| 130 | + | is stopped and answered with a 503 page that says so. Static assets are | |
| 131 | + | served without running your Worker, and count toward neither. | |
| 127 | 132 | - `vars` are deployed as plain-text bindings (or JSON, for objects). Rows | |
| 128 | 133 | of the project's [secrets and variables](/guides/secrets-and-variables/) | |
| 129 | 134 | available to Deployments are bound too, and replace a `var` of the same |
| 5 | 5 | * Platforms namespace (`web-git-fix-login-acme.g1t.page` is the fix-login | |
| 6 | 6 | * branch's preview of acme's web project), so the app is fetched by name | |
| 7 | 7 | * and runs only for as long as it answers. An app no one visits runs | |
| 8 | − | * nothing and costs nothing. The one lookup is for an address an app | |
| 8 | + | * nothing and costs nothing. Each request it serves is held to | |
| 9 | + | * `APP_LIMITS`, so one app cannot spend without bound. The one lookup is for an address an app | |
| 9 | 10 | * had before its project moved: `DOMAINS` holds a redirect under the old | |
| 10 | 11 | * hostname for as long as the old name is held, and it is followed before | |
| 11 | 12 | * anything the old script would answer (such as a paused notice). | |
| ⋯ | |||
| 18 | 19 | * people sign in to. | |
| 19 | 20 | */ | |
| 20 | 21 | ||
| 21 | − | import { DOMAIN, FALLBACK, parseEntry, redirectTo, route, type DomainEntry } from "./route.ts"; | |
| 22 | + | import { APP_LIMITS, DOMAIN, FALLBACK, overLimits, parseEntry, redirectTo, route, type DomainEntry } from "./route.ts"; | |
| 22 | 23 | ||
| 23 | 24 | type Env = { | |
| 24 | 25 | APPS: DispatchNamespace; | |
| ⋯ | |||
| 76 | 77 | async function dispatch(env: Env, request: Request, script: string, shown: string, missing: () => Response): Promise<Response> { | |
| 77 | 78 | let app: Fetcher; | |
| 78 | 79 | try { | |
| 79 | − | app = env.APPS.get(script); | |
| 80 | + | app = env.APPS.get(script, {}, { limits: APP_LIMITS }); | |
| 80 | 81 | } catch { | |
| 81 | 82 | return missing(); | |
| 82 | 83 | } | |
| ⋯ | |||
| 84 | 85 | return await app.fetch(request); | |
| 85 | 86 | } catch (error) { | |
| 86 | 87 | if (/worker not found|script not found|does not exist/i.test(String(error))) return missing(); | |
| 88 | + | if (overLimits(error)) { | |
| 89 | + | return page( | |
| 90 | + | 503, | |
| 91 | + | "The app went over its limits", | |
| 92 | + | `<p>The app at <code>${escape(shown)}</code> used more than ${APP_LIMITS.cpuMs} ms of CPU time, or made more than ${APP_LIMITS.subRequests} requests of its own, answering this request.</p>`, | |
| 93 | + | shown, | |
| 94 | + | ); | |
| 95 | + | } | |
| 87 | 96 | return page( | |
| 88 | 97 | 502, | |
| 89 | 98 | "The app failed", | |
| 1 | 1 | import assert from "node:assert/strict"; | |
| 2 | 2 | import { test } from "node:test"; | |
| 3 | 3 | ||
| 4 | − | import { parseEntry, redirectTo, route } from "./route.ts"; | |
| 4 | + | import { APP_LIMITS, overLimits, parseEntry, redirectTo, route } from "./route.ts"; | |
| 5 | 5 | import worker from "./index.ts"; | |
| 6 | 6 | ||
| 7 | 7 | test("hosts on g1t.page are the home page, the fallback origin, or an app", () => { | |
| ⋯ | |||
| 124 | 124 | }; | |
| 125 | 125 | assert.equal(await (await call("https://shop-acme.g1t.page/", e)).text(), "shop"); | |
| 126 | 126 | }); | |
| 127 | + | ||
| 128 | + | test("every app runs within the limits, and one that goes over them says so", async () => { | |
| 129 | + | const asked: unknown[] = []; | |
| 130 | + | const e = { | |
| 131 | + | APPS: { | |
| 132 | + | get(_name: string, _args: unknown, options: unknown) { | |
| 133 | + | asked.push(options); | |
| 134 | + | return { | |
| 135 | + | async fetch() { | |
| 136 | + | throw new Error("Worker exceeded CPU time limit."); | |
| 137 | + | }, | |
| 138 | + | }; | |
| 139 | + | }, | |
| 140 | + | }, | |
| 141 | + | } as any; | |
| 142 | + | const response = await call("https://web-acme.g1t.page/", e); | |
| 143 | + | assert.deepEqual(asked, [{ limits: APP_LIMITS }]); | |
| 144 | + | assert.equal(response.status, 503); | |
| 145 | + | assert.match(await response.text(), /went over its limits/); | |
| 146 | + | assert.ok(overLimits(new Error("Too many subrequests."))); | |
| 147 | + | assert.ok(!overLimits(new Error("TypeError: x is undefined"))); | |
| 148 | + | }); | |
| 47 | 47 | const url = new URL(requestUrl); | |
| 48 | 48 | return `https://${target}${url.pathname}${url.search}`; | |
| 49 | 49 | } | |
| 50 | + | ||
| 51 | + | /** | |
| 52 | + | * What one request to an app may use: CPU time, not counting time spent | |
| 53 | + | * waiting on the network, and requests it makes of its own. Static assets | |
| 54 | + | * are served without running the app, and use neither. No plan sets | |
| 55 | + | * others; the Deployments guide names these. | |
| 56 | + | */ | |
| 57 | + | export const APP_LIMITS = { cpuMs: 50, subRequests: 50 } as const; | |
| 58 | + | ||
| 59 | + | /** Whether an app's failure was going over `APP_LIMITS`, as the runtime words it. */ | |
| 60 | + | export function overLimits(error: unknown): boolean { | |
| 61 | + | return /exceeded (its )?cpu|cpu time limit|too many subrequests|subrequest limit/i.test(String(error)); | |
| 62 | + | } |