diff --git a/frontend/tests/support/fixtures.ts b/frontend/tests/support/fixtures.ts index cebf92f5a..f04fe8c0b 100644 --- a/frontend/tests/support/fixtures.ts +++ b/frontend/tests/support/fixtures.ts @@ -32,6 +32,11 @@ export const test = base.extend<{ authenticatedPage: async ({page, apiContext, currentUser}, use) => { const {token} = await login(page, apiContext, currentUser) await use(page) + // Navigate away to stop all frontend requests (notification polling, token + // refresh, etc.) before the next test's fixture setup seeds the database. + // Without this, the previous test's page can hold DB connections via API + // requests, starving the next test's Factory.seed() PATCH call. + await page.goto('about:blank').catch(() => {}) }, }) diff --git a/pkg/events/events.go b/pkg/events/events.go index 00599f9d9..4ca7be72d 100644 --- a/pkg/events/events.go +++ b/pkg/events/events.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "sync" "time" "github.com/getsentry/sentry-go" @@ -36,6 +37,19 @@ import ( var pubsub *gochannel.GoChannel +// activeHandlers tracks in-flight event handler goroutines so the test +// endpoint can wait for them to finish before truncating tables. +var activeHandlers sync.WaitGroup + +// WaitForPendingHandlers blocks until all currently in-flight event handler +// goroutines have completed (including retries). This is intended for the +// testing endpoint to avoid connection starvation: async handlers from the +// previous test can hold SQLite connections, starving the next test's seed +// request. +func WaitForPendingHandlers() { + activeHandlers.Wait() +} + // Event represents the event interface used by all events type Event interface { Name() string @@ -90,7 +104,19 @@ func InitEvents() (err error) { return nil }) + // handlerTracker is a middleware that tracks in-flight handlers via the + // activeHandlers WaitGroup. It wraps the entire processing chain + // (including retries) so WaitForPendingHandlers() can drain all work. + handlerTracker := func(h message.HandlerFunc) message.HandlerFunc { + return func(msg *message.Message) ([]*message.Message, error) { + activeHandlers.Add(1) + defer activeHandlers.Done() + return h(msg) + } + } + router.AddMiddleware( + handlerTracker, poison, middleware.Retry{ MaxRetries: 5, diff --git a/pkg/routes/api/v1/testing.go b/pkg/routes/api/v1/testing.go index 6f32d2395..53631845b 100644 --- a/pkg/routes/api/v1/testing.go +++ b/pkg/routes/api/v1/testing.go @@ -23,6 +23,7 @@ import ( "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/events" "code.vikunja.io/api/pkg/log" "github.com/labstack/echo/v5" @@ -61,6 +62,11 @@ func HandleTesting(c *echo.Context) error { }) } + // Wait for all async event handlers from the previous test to complete + // before modifying the database. Without this, handlers hold SQLite + // connections and starve this request's truncate/insert operations. + events.WaitForPendingHandlers() + truncate := c.QueryParam("truncate") if truncate == "true" || truncate == "" { err = db.RestoreAndTruncate(table, content)