From 9ee232b18d02bd49b13de01ed2e430b134267124 Mon Sep 17 00:00:00 2001 From: Oleks Date: Fri, 15 May 2026 23:46:46 +0300 Subject: [PATCH] fix(project_events): use process-lifetime ctx for async publish The publish goroutine inherited the request context via context.WithoutCancel. That context carries a request-scoped DB session returned to the pool when the HTTP handler completes, so GetProjectByID + access checks in the goroutine raced with session teardown and intermittently returned empty recipient sets (uids=[]), silently dropping every SSE board event. Root the detached context in graceful.ShutdownContext() (global engine, process lifetime). (cherry picked from commit bfc10289e60af9a1b7ab976143df559b16805cb7) --- services/project_events/events.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/services/project_events/events.go b/services/project_events/events.go index b86bbf735d..e168f6dcd1 100644 --- a/services/project_events/events.go +++ b/services/project_events/events.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/eventsource" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" ) @@ -317,8 +318,16 @@ func PublishProjectDeleted(ctx context.Context, payload ProjectDeleted) { go publishEvent(detach(ctx), payload.ProjectID, payload) } -// detach strips cancellation/deadline from ctx but preserves stored -// values (notably the session tag) so the goroutine outlives the request. -func detach(ctx context.Context) context.Context { - return context.WithoutCancel(ctx) +// detach returns a context safe for use in the fire-and-forget publish +// goroutine. The request's context carries a request-scoped DB session +// that is returned to the pool once the HTTP handler completes; reusing +// it from the goroutine races with that teardown and makes subsequent +// queries (GetProjectByID, access checks) fail intermittently. The +// session tag is already resolved synchronously before the goroutine +// starts, so the goroutine needs no request-scoped values — only a +// clean, process-lifetime DB context. ShutdownContext is backed by the +// global engine, outlives any single request, and is cancelled on app +// shutdown so we don't leak goroutines past teardown. +func detach(_ context.Context) context.Context { + return graceful.GetManager().ShutdownContext() }