diff --git a/services/project_events/events.go b/services/project_events/events.go index 15efb7f64f..9939ac7f52 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" ) @@ -318,8 +319,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() }