Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 516
feat(cli): inject function slug into served fns#6345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base:develop
Are you sure you want to change the base?
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
2989c69954c972c24a039cbc9aecabc0a94File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -122,6 +122,29 @@ const functionsConfig: Record<string, FunctionConfig> = (() => { | ||
| } | ||
| })(); | ||
| // EdgeRuntime.userWorkers.create() pools workers by servicePath alone, so two configured | ||
| // function names whose entrypoints live in the same directory would share one cached | ||
| // worker's environment (and thus its SUPABASE_FUNCTION_SLUG). For those shared directories | ||
| // only, serialize worker creation per servicePath and force a fresh worker whenever the | ||
| // requested function differs from whichever one currently owns the cached worker. Module | ||
| // resolution keeps using each function's real entrypoint path, so imports that reach | ||
| // outside the directory (e.g. a sibling `_shared` folder) are unaffected. | ||
| const sharedServicePaths = (() => { | ||
| const counts = new Map<string, number>(); | ||
| for (const config of Object.values(functionsConfig)) { | ||
| const servicePath = dirname(config.entrypointPath); | ||
| counts.set(servicePath, (counts.get(servicePath) ?? 0) + 1); | ||
| } | ||
| return new Set( | ||
| Array.from(counts) | ||
| .filter(([, count]) => count > 1) | ||
| .map(([servicePath]) => servicePath), | ||
| ); | ||
| })(); | ||
| const sharedWorkerOwners = new Map<string, string>(); | ||
| const sharedWorkerQueues = new Map<string, Promise<void>>(); | ||
| /* --- JWT verification --- */ | ||
| export function extractBearerToken(rawToken: string) { | ||
| const tokenParts = rawToken.split(" "); | ||
| @@ -331,6 +354,8 @@ Deno.serve({ | ||
| ([name, _]) => !name.startsWith("SUPABASE_"), | ||
| ), | ||
| ), | ||
| // Listed after the spreads so neither the container env nor function config can shadow it | ||
| SUPABASE_FUNCTION_SLUG: functionName, | ||
raulb marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| }; | ||
| if (SUPABASE_PUBLISHABLE_KEY) { | ||
| envVarsObj["SUPABASE_PUBLISHABLE_KEYS"] = JSON.stringify({ | ||
| @@ -347,7 +372,6 @@ Deno.serve({ | ||
| ([name, _]) => !EXCLUDED_ENVS.includes(name) && !name.startsWith("SUPABASE_INTERNAL_"), | ||
| ); | ||
| const forceCreate = false; | ||
| const customModuleRoot = ""; // empty string to allow any local path | ||
| const cpuTimeSoftLimitMs = 1000; | ||
| const cpuTimeHardLimitMs = 2000; | ||
| @@ -364,26 +388,53 @@ Deno.serve({ | ||
| const staticPatterns = functionsConfig[functionName].staticFiles; | ||
| try { | ||
| const worker = await EdgeRuntime.userWorkers.create({ | ||
| servicePath, | ||
| memoryLimitMb, | ||
| workerTimeoutMs, | ||
| noModuleCache, | ||
| noNpm: !usePackageJson, | ||
| importMapPath: functionsConfig[functionName].importMapPath, | ||
| envVars, | ||
| forceCreate, | ||
| customModuleRoot, | ||
| cpuTimeSoftLimitMs, | ||
| cpuTimeHardLimitMs, | ||
| decoratorType, | ||
| maybeEntrypoint, | ||
| context: { | ||
| useReadSyncFileAPI: true, | ||
| }, | ||
| staticPatterns, | ||
| // Only shared directories pay for coordination: serialize worker creation for this | ||
| // servicePath so a concurrent request for a different function can't race the pool's | ||
| // "which slug currently owns this worker" decision, then force a fresh worker only | ||
| // when the owner actually changed. | ||
| let forceCreate = false; | ||
| let releaseSharedWorkerSlot: (() => void) | undefined; | ||
| if (sharedServicePaths.has(servicePath)) { | ||
| const previousSlot = sharedWorkerQueues.get(servicePath) ?? Promise.resolve(); | ||
| let releaseSlot: () => void; | ||
| const currentSlot = new Promise<void>((resolve) => { | ||
| releaseSlot = resolve; | ||
| }); | ||
| sharedWorkerQueues.set( | ||
| servicePath, | ||
| previousSlot.then(() => currentSlot), | ||
| ); | ||
| await previousSlot; | ||
| releaseSharedWorkerSlot = releaseSlot!; | ||
| forceCreate = sharedWorkerOwners.get(servicePath) !== functionName; | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] The owner map does not identify the worker Edge Runtime will reuse
| ||
| sharedWorkerOwners.set(servicePath, functionName); | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Do not record ownership before worker creation succeeds If B’s forced | ||
| } | ||
| try { | ||
| let worker; | ||
| try { | ||
| worker = await EdgeRuntime.userWorkers.create({ | ||
| servicePath, | ||
| memoryLimitMb, | ||
| workerTimeoutMs, | ||
| noModuleCache, | ||
| noNpm: !usePackageJson, | ||
| importMapPath: functionsConfig[functionName].importMapPath, | ||
| envVars, | ||
| forceCreate, | ||
| customModuleRoot, | ||
| cpuTimeSoftLimitMs, | ||
| cpuTimeHardLimitMs, | ||
| decoratorType, | ||
| maybeEntrypoint, | ||
| context: { | ||
| useReadSyncFileAPI: true, | ||
| }, | ||
| staticPatterns, | ||
| }); | ||
| } finally { | ||
| releaseSharedWorkerSlot?.(); | ||
Comment on lines
+435
to
+436
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When requests for two function names sharing a AGENTS.md reference: AGENTS.md:L142-L148 Useful? React with 👍 / 👎. | ||
| } | ||
| const userReq = prepareUserRequest(req); | ||
| return await worker.fetch(userReq); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -171,11 +171,8 @@ | ||
| return new URL(`file://${path}`).href; | ||
| } | ||
| async function serveFunction(req: Request, config: any, functionName: string, functionConfig: any) { | ||
| const authError = await verifyRequest(req, config, functionConfig); | ||
| if (authError) return authError; | ||
| const envVars = Object.entries({ | ||
| export function buildFunctionEnv(config: any, functionConfig: any, functionName: string) { | ||
| return { | ||
| ...config.env, | ||
| ...functionConfig.env, | ||
| SUPABASE_URL: config.supabaseUrl, | ||
| @@ -184,26 +181,86 @@ | ||
| SUPABASE_DB_URL: config.dbUrl, | ||
| SUPABASE_PUBLISHABLE_KEYS: JSON.stringify({ default: config.publishableKey }), | ||
| SUPABASE_SECRET_KEYS: JSON.stringify({ default: config.secretKey }), | ||
| }); | ||
| SUPABASE_FUNCTION_SLUG: functionName, | ||
| }; | ||
| } | ||
| try { | ||
| const worker = await EdgeRuntime.userWorkers.create({ | ||
| servicePath: dirname(functionConfig.entrypointPath), | ||
| memoryLimitMb: 256, | ||
| workerTimeoutMs: 400000, | ||
| noModuleCache: false, | ||
| noNpm: false, | ||
| importMapPath: functionConfig.importMapPath ?? undefined, | ||
| envVars, | ||
| forceCreate: false, | ||
| customModuleRoot: "", | ||
| cpuTimeSoftLimitMs: 1000, | ||
| cpuTimeHardLimitMs: 2000, | ||
| decoratorType: "tc39", | ||
| maybeEntrypoint: fileUrl(functionConfig.entrypointPath), | ||
| context: { useReadSyncFileAPI: true }, | ||
| staticPatterns: functionConfig.staticFiles, | ||
| // EdgeRuntime.userWorkers.create() pools workers by servicePath alone, so two configured | ||
| // functions whose entrypoints live in the same directory would share one cached worker's | ||
| // environment (and thus its SUPABASE_FUNCTION_SLUG). For those shared directories only, | ||
| // serialize worker creation per servicePath and force a fresh worker whenever the | ||
| // requested function differs from whichever one currently owns the cached worker. Module | ||
| // resolution keeps using each function's real entrypoint path, so imports that reach | ||
| // outside the directory (e.g. a sibling `_shared` folder) are unaffected. | ||
| function computeSharedServicePaths(functions: Record<string, { entrypointPath: string }>) { | ||
| const counts = new Map<string, number>(); | ||
| for (const functionConfig of Object.values(functions)) { | ||
| const servicePath = dirname(functionConfig.entrypointPath); | ||
| counts.set(servicePath, (counts.get(servicePath) ?? 0) + 1); | ||
| } | ||
| return new Set( | ||
| Array.from(counts) | ||
| .filter(([, count]) => count > 1) | ||
| .map(([servicePath]) => servicePath), | ||
| ); | ||
| } | ||
| const sharedWorkerOwners = new Map<string, string>(); | ||
| const sharedWorkerQueues = new Map<string, Promise<void>>(); | ||
| async function serveFunction(req: Request, config: any, functionName: string, functionConfig: any) { | ||
| const authError = await verifyRequest(req, config, functionConfig); | ||
| if (authError) return authError; | ||
| const envVars = Object.entries(buildFunctionEnv(config, functionConfig, functionName)); | ||
| const servicePath = dirname(functionConfig.entrypointPath); | ||
| const sharedServicePaths = computeSharedServicePaths(config.functions ?? {}); | ||
| // Only shared directories pay for coordination: serialize worker creation for this | ||
| // servicePath so a concurrent request for a different function can't race the pool's | ||
| // "which slug currently owns this worker" decision, then force a fresh worker only | ||
| // when the owner actually changed. | ||
| let forceCreate = false; | ||
| let releaseSharedWorkerSlot: (() => void) | undefined; | ||
| if (sharedServicePaths.has(servicePath)) { | ||
| const previousSlot = sharedWorkerQueues.get(servicePath) ?? Promise.resolve(); | ||
| let releaseSlot: () => void; | ||
| const currentSlot = new Promise<void>((resolve) => { | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking: this manual Promise gate currently fails the required quality check
| ||
| releaseSlot = resolve; | ||
| }); | ||
| sharedWorkerQueues.set( | ||
| servicePath, | ||
Check warning on line 232 in packages/stack/src/services/edge-runtime-main.ts
| ||
| previousSlot.then(() => currentSlot), | ||
| ); | ||
| await previousSlot; | ||
| releaseSharedWorkerSlot = releaseSlot!; | ||
| forceCreate = sharedWorkerOwners.get(servicePath) !== functionName; | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Apply the stable worker-identity fix in the stack path too This owner comparison duplicates the CLI serve-path bug: after multiple forced creations, Edge Runtime retains multiple active workers for this | ||
| sharedWorkerOwners.set(servicePath, functionName); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Severity: MEDIUM A caller can select a function whose worker fails during creation. This records the URL-selected function as the owner before the cache is replaced; a later request for that function skips 💡 Fix SuggestionSuggestion: The root cause is that The fix requires two coordinated changes:
worker=awaitEdgeRuntime.userWorkers.create({ ... });// Only record ownership after the worker is confirmed running:sharedWorkerOwners.set(servicePath,functionName);}finally{releaseSharedWorkerSlot?.();}With this change, a failed | ||
| } | ||
| try { | ||
| let worker; | ||
| try { | ||
| worker = await EdgeRuntime.userWorkers.create({ | ||
| servicePath, | ||
| memoryLimitMb: 256, | ||
| workerTimeoutMs: 400000, | ||
| noModuleCache: false, | ||
| noNpm: false, | ||
| importMapPath: functionConfig.importMapPath ?? undefined, | ||
| envVars, | ||
| forceCreate, | ||
| customModuleRoot: "", | ||
| cpuTimeSoftLimitMs: 1000, | ||
| cpuTimeHardLimitMs: 2000, | ||
| decoratorType: "tc39", | ||
| maybeEntrypoint: fileUrl(functionConfig.entrypointPath), | ||
| context: { useReadSyncFileAPI: true }, | ||
| staticPatterns: functionConfig.staticFiles, | ||
| }); | ||
| } finally { | ||
| releaseSharedWorkerSlot?.(); | ||
| } | ||
| return await worker.fetch(req); | ||
| } catch (error) { | ||
| console.error(`Failed to serve Function ${functionName}`, error); | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.