-
Notifications
You must be signed in to change notification settings - Fork 441
fix: address flaky integration tests #7890
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: main
Are you sure you want to change the base?
Changes from all commits
aff59d3
8380694
9d34d22
f317f23
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -100,6 +100,8 @@ export class EdgeFunctionsRegistry { | |
|
|
||
| private aiGatewayContext?: AIGatewayContext | null | ||
| private buildError: Error | null = null | ||
| private buildPending = false | ||
| private buildPromise: Promise<{ warnings: Record<string, string[]> }> | null = null | ||
| private bundler: typeof import('@netlify/edge-bundler') | ||
| private configPath: string | ||
| private importMapFromTOML?: string | ||
|
|
@@ -181,7 +183,44 @@ export class EdgeFunctionsRegistry { | |
| return [...this.internalFunctions, ...this.userFunctions] | ||
| } | ||
|
|
||
| private async build() { | ||
| // Note: We intentionally don't use @netlify/dev-utils memoize() here because | ||
| // it has a 300ms debounce and fire-and-forget logic. Edge function build | ||
| // needs callers to receive the latest build result | ||
| private async build(): Promise<{ warnings: Record<string, string[]> }> { | ||
| // If a build is already in progress, mark that we need another build | ||
| // and return the current build's promise. The running build will | ||
| // trigger a rebuild when it completes if buildPending is true. | ||
| if (this.buildPromise) { | ||
|
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. can we add tests here for this method which will ensure this tests fine?
Member
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. ^ and the test should verify that subsequent calls trigger an actual new build! |
||
| this.buildPending = true | ||
| return this.buildPromise | ||
| } | ||
|
|
||
| this.buildPending = false | ||
| this.buildPromise = this.doBuild() | ||
|
|
||
| try { | ||
| const result = await this.buildPromise | ||
| this.buildPromise = null | ||
|
|
||
| // If another build was requested while we were building, run it now | ||
| if (this.buildPending) { | ||
| return await this.build() | ||
| } | ||
|
|
||
| return result | ||
| } catch (error) { | ||
| this.buildPromise = null | ||
|
|
||
| // If another build was requested while we were building, run it now | ||
| if (this.buildPending) { | ||
| return await this.build() | ||
| } | ||
|
|
||
| throw error | ||
| } | ||
| } | ||
|
|
||
| private async doBuild(): Promise<{ warnings: Record<string, string[]> }> { | ||
| const warnings: Record<string, string[]> = {} | ||
|
|
||
| try { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import { describe, expect, test, vi } from 'vitest' | ||
|
|
||
| import { EdgeFunctionsRegistry } from '../../../../src/lib/edge-functions/registry.js' | ||
|
|
||
| /** | ||
| * Tests for EdgeFunctionsRegistry.build() coalescing behavior. | ||
| */ | ||
| describe('EdgeFunctionsRegistry.build() coalescing', () => { | ||
| const createMockRegistry = () => { | ||
| const state = { buildCount: 0, shouldFail: false } | ||
|
|
||
| // Create instance with minimal mocked dependencies | ||
| const registry = Object.create(EdgeFunctionsRegistry.prototype) as InstanceType<typeof EdgeFunctionsRegistry> | ||
|
|
||
| // Initialize only the properties needed for build() | ||
| // @ts-expect-error -- accessing private members for testing | ||
| registry.buildPending = false | ||
| // @ts-expect-error -- accessing private members for testing | ||
| registry.buildPromise = null | ||
| // @ts-expect-error -- accessing private members for testing | ||
| registry.doBuild = vi.fn(async () => { | ||
| state.buildCount++ | ||
| await new Promise((resolve) => setTimeout(resolve, 10)) | ||
| if (state.shouldFail) { | ||
| state.shouldFail = false | ||
| throw new Error('Build failed') | ||
| } | ||
| return { warnings: {} } | ||
| }) | ||
|
|
||
| return { registry, state } | ||
| } | ||
|
|
||
| test('concurrent calls coalesce into fewer builds', async () => { | ||
| const { registry, state } = createMockRegistry() | ||
|
|
||
| // @ts-expect-error -- accessing private method for testing | ||
| const results = await Promise.all([registry.build(), registry.build(), registry.build()]) | ||
|
|
||
| expect(results).toHaveLength(3) | ||
| for (const r of results) { | ||
| expect(r).toEqual({ warnings: {} }) | ||
| } | ||
| // First build + one rebuild for pending = 2 total | ||
| expect(state.buildCount).toBe(2) | ||
|
|
||
| // Subsequent call after all concurrent calls complete triggers a NEW build | ||
| // @ts-expect-error -- accessing private method for testing | ||
| await registry.build() | ||
| expect(state.buildCount).toBe(3) | ||
| }) | ||
|
|
||
| test('retries pending build on failure', async () => { | ||
| const { registry, state } = createMockRegistry() | ||
| state.shouldFail = true | ||
|
|
||
| // @ts-expect-error -- accessing private method for testing | ||
| const [result1, result2] = await Promise.allSettled([registry.build(), registry.build()]) | ||
|
|
||
| expect(result1.status).toBe('fulfilled') // First call gets retry result | ||
| expect(result2.status).toBe('rejected') // Concurrent call gets the original failure | ||
| expect(state.buildCount).toBe(2) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π Instead of reimplementing this, we could reuse the existing battle-tested helper we already use for this same purpose for (non-Edge) Functions! Check it out:
cli/src/lib/functions/runtimes/js/builders/zisi.ts
Line 75 in 321170d
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea thank you!
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I looked into implementing that and unfortunately if build A fails while build B is pending, the caller of build A gets the error but build B runs without anyone waiting for it. I'll add a comment explaining why there is a custom implementation