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 bfc10289e6)
This commit is contained in:
Oleks
2026-05-15 23:46:46 +03:00
parent 9c1699feb5
commit 9ee232b18d
+13 -4
View File
@@ -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()
}