Skip to content

Commit 6d5aa92

Browse files
authored
Refactor request context (#32956)
Introduce RequestContext: is a short-lived context that is used to store request-specific data. RequestContext could be used to clean form tmp files, close context git repo, and do some tracing in the future. Then a lot of legacy code could be removed or improved. For example: most `ctx.Repo.GitRepo.Close()` could be removed because the git repo could be closed when the request is done.
1 parent 781c6df commit 6d5aa92

34 files changed

+376
-419
lines changed

modules/gitrepo/gitrepo.go

+18-48
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111

1212
"code.gitea.io/gitea/modules/git"
13+
"code.gitea.io/gitea/modules/reqctx"
1314
"code.gitea.io/gitea/modules/setting"
1415
"code.gitea.io/gitea/modules/util"
1516
)
@@ -38,63 +39,32 @@ func OpenWikiRepository(ctx context.Context, repo Repository) (*git.Repository,
3839

3940
// contextKey is a value for use with context.WithValue.
4041
type contextKey struct {
41-
name string
42-
}
43-
44-
// RepositoryContextKey is a context key. It is used with context.Value() to get the current Repository for the context
45-
var RepositoryContextKey = &contextKey{"repository"}
46-
47-
// RepositoryFromContext attempts to get the repository from the context
48-
func repositoryFromContext(ctx context.Context, repo Repository) *git.Repository {
49-
value := ctx.Value(RepositoryContextKey)
50-
if value == nil {
51-
return nil
52-
}
53-
54-
if gitRepo, ok := value.(*git.Repository); ok && gitRepo != nil {
55-
if gitRepo.Path == repoPath(repo) {
56-
return gitRepo
57-
}
58-
}
59-
60-
return nil
42+
repoPath string
6143
}
6244

6345
// RepositoryFromContextOrOpen attempts to get the repository from the context or just opens it
6446
func RepositoryFromContextOrOpen(ctx context.Context, repo Repository) (*git.Repository, io.Closer, error) {
65-
gitRepo := repositoryFromContext(ctx, repo)
66-
if gitRepo != nil {
67-
return gitRepo, util.NopCloser{}, nil
47+
ds := reqctx.GetRequestDataStore(ctx)
48+
if ds != nil {
49+
gitRepo, err := RepositoryFromRequestContextOrOpen(ctx, ds, repo)
50+
return gitRepo, util.NopCloser{}, err
6851
}
69-
7052
gitRepo, err := OpenRepository(ctx, repo)
7153
return gitRepo, gitRepo, err
7254
}
7355

74-
// repositoryFromContextPath attempts to get the repository from the context
75-
func repositoryFromContextPath(ctx context.Context, path string) *git.Repository {
76-
value := ctx.Value(RepositoryContextKey)
77-
if value == nil {
78-
return nil
56+
// RepositoryFromRequestContextOrOpen opens the repository at the given relative path in the provided request context
57+
// The repo will be automatically closed when the request context is done
58+
func RepositoryFromRequestContextOrOpen(ctx context.Context, ds reqctx.RequestDataStore, repo Repository) (*git.Repository, error) {
59+
ck := contextKey{repoPath: repoPath(repo)}
60+
if gitRepo, ok := ctx.Value(ck).(*git.Repository); ok {
61+
return gitRepo, nil
7962
}
80-
81-
if repo, ok := value.(*git.Repository); ok && repo != nil {
82-
if repo.Path == path {
83-
return repo
84-
}
63+
gitRepo, err := git.OpenRepository(ctx, ck.repoPath)
64+
if err != nil {
65+
return nil, err
8566
}
86-
87-
return nil
88-
}
89-
90-
// RepositoryFromContextOrOpenPath attempts to get the repository from the context or just opens it
91-
// Deprecated: Use RepositoryFromContextOrOpen instead
92-
func RepositoryFromContextOrOpenPath(ctx context.Context, path string) (*git.Repository, io.Closer, error) {
93-
gitRepo := repositoryFromContextPath(ctx, path)
94-
if gitRepo != nil {
95-
return gitRepo, util.NopCloser{}, nil
96-
}
97-
98-
gitRepo, err := git.OpenRepository(ctx, path)
99-
return gitRepo, gitRepo, err
67+
ds.AddCloser(gitRepo)
68+
ds.SetContextValue(ck, gitRepo)
69+
return gitRepo, nil
10070
}

modules/gitrepo/walk_gogit.go

+4-8
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,11 @@ import (
1414
// WalkReferences walks all the references from the repository
1515
// refname is empty, ObjectTag or ObjectBranch. All other values should be treated as equivalent to empty.
1616
func WalkReferences(ctx context.Context, repo Repository, walkfn func(sha1, refname string) error) (int, error) {
17-
gitRepo := repositoryFromContext(ctx, repo)
18-
if gitRepo == nil {
19-
var err error
20-
gitRepo, err = OpenRepository(ctx, repo)
21-
if err != nil {
22-
return 0, err
23-
}
24-
defer gitRepo.Close()
17+
gitRepo, closer, err := RepositoryFromContextOrOpen(ctx, repo)
18+
if err != nil {
19+
return 0, err
2520
}
21+
defer closer.Close()
2622

2723
i := 0
2824
iter, err := gitRepo.GoGitRepo().References()

modules/reqctx/datastore.go

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package reqctx
5+
6+
import (
7+
"context"
8+
"io"
9+
"sync"
10+
11+
"code.gitea.io/gitea/modules/process"
12+
)
13+
14+
type ContextDataProvider interface {
15+
GetData() ContextData
16+
}
17+
18+
type ContextData map[string]any
19+
20+
func (ds ContextData) GetData() ContextData {
21+
return ds
22+
}
23+
24+
func (ds ContextData) MergeFrom(other ContextData) ContextData {
25+
for k, v := range other {
26+
ds[k] = v
27+
}
28+
return ds
29+
}
30+
31+
// RequestDataStore is a short-lived context-related object that is used to store request-specific data.
32+
type RequestDataStore interface {
33+
GetData() ContextData
34+
SetContextValue(k, v any)
35+
GetContextValue(key any) any
36+
AddCleanUp(f func())
37+
AddCloser(c io.Closer)
38+
}
39+
40+
type requestDataStoreKeyType struct{}
41+
42+
var RequestDataStoreKey requestDataStoreKeyType
43+
44+
type requestDataStore struct {
45+
data ContextData
46+
47+
mu sync.RWMutex
48+
values map[any]any
49+
cleanUpFuncs []func()
50+
}
51+
52+
func (r *requestDataStore) GetContextValue(key any) any {
53+
if key == RequestDataStoreKey {
54+
return r
55+
}
56+
r.mu.RLock()
57+
defer r.mu.RUnlock()
58+
return r.values[key]
59+
}
60+
61+
func (r *requestDataStore) SetContextValue(k, v any) {
62+
r.mu.Lock()
63+
r.values[k] = v
64+
r.mu.Unlock()
65+
}
66+
67+
// GetData and the underlying ContextData are not thread-safe, callers should ensure thread-safety.
68+
func (r *requestDataStore) GetData() ContextData {
69+
if r.data == nil {
70+
r.data = make(ContextData)
71+
}
72+
return r.data
73+
}
74+
75+
func (r *requestDataStore) AddCleanUp(f func()) {
76+
r.mu.Lock()
77+
r.cleanUpFuncs = append(r.cleanUpFuncs, f)
78+
r.mu.Unlock()
79+
}
80+
81+
func (r *requestDataStore) AddCloser(c io.Closer) {
82+
r.AddCleanUp(func() { _ = c.Close() })
83+
}
84+
85+
func (r *requestDataStore) cleanUp() {
86+
for _, f := range r.cleanUpFuncs {
87+
f()
88+
}
89+
}
90+
91+
func GetRequestDataStore(ctx context.Context) RequestDataStore {
92+
if req, ok := ctx.Value(RequestDataStoreKey).(*requestDataStore); ok {
93+
return req
94+
}
95+
return nil
96+
}
97+
98+
type requestContext struct {
99+
context.Context
100+
dataStore *requestDataStore
101+
}
102+
103+
func (c *requestContext) Value(key any) any {
104+
if v := c.dataStore.GetContextValue(key); v != nil {
105+
return v
106+
}
107+
return c.Context.Value(key)
108+
}
109+
110+
func NewRequestContext(parentCtx context.Context, profDesc string) (_ context.Context, finished func()) {
111+
ctx, _, processFinished := process.GetManager().AddTypedContext(parentCtx, profDesc, process.RequestProcessType, true)
112+
reqCtx := &requestContext{Context: ctx, dataStore: &requestDataStore{values: make(map[any]any)}}
113+
return reqCtx, func() {
114+
reqCtx.dataStore.cleanUp()
115+
processFinished()
116+
}
117+
}
118+
119+
// NewRequestContextForTest creates a new RequestContext for testing purposes
120+
// It doesn't add the context to the process manager, nor do cleanup
121+
func NewRequestContextForTest(parentCtx context.Context) context.Context {
122+
return &requestContext{Context: parentCtx, dataStore: &requestDataStore{values: make(map[any]any)}}
123+
}

modules/web/handler.go

+6-20
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
package web
55

66
import (
7-
goctx "context"
87
"fmt"
98
"net/http"
109
"reflect"
@@ -51,7 +50,6 @@ func (r *responseWriter) WriteHeader(statusCode int) {
5150
var (
5251
httpReqType = reflect.TypeOf((*http.Request)(nil))
5352
respWriterType = reflect.TypeOf((*http.ResponseWriter)(nil)).Elem()
54-
cancelFuncType = reflect.TypeOf((*goctx.CancelFunc)(nil)).Elem()
5553
)
5654

5755
// preCheckHandler checks whether the handler is valid, developers could get first-time feedback, all mistakes could be found at startup
@@ -65,11 +63,8 @@ func preCheckHandler(fn reflect.Value, argsIn []reflect.Value) {
6563
if !hasStatusProvider {
6664
panic(fmt.Sprintf("handler should have at least one ResponseStatusProvider argument, but got %s", fn.Type()))
6765
}
68-
if fn.Type().NumOut() != 0 && fn.Type().NumIn() != 1 {
69-
panic(fmt.Sprintf("handler should have no return value or only one argument, but got %s", fn.Type()))
70-
}
71-
if fn.Type().NumOut() == 1 && fn.Type().Out(0) != cancelFuncType {
72-
panic(fmt.Sprintf("handler should return a cancel function, but got %s", fn.Type()))
66+
if fn.Type().NumOut() != 0 {
67+
panic(fmt.Sprintf("handler should have no return value other than registered ones, but got %s", fn.Type()))
7368
}
7469
}
7570

@@ -105,16 +100,10 @@ func prepareHandleArgsIn(resp http.ResponseWriter, req *http.Request, fn reflect
105100
return argsIn
106101
}
107102

108-
func handleResponse(fn reflect.Value, ret []reflect.Value) goctx.CancelFunc {
109-
if len(ret) == 1 {
110-
if cancelFunc, ok := ret[0].Interface().(goctx.CancelFunc); ok {
111-
return cancelFunc
112-
}
113-
panic(fmt.Sprintf("unsupported return type: %s", ret[0].Type()))
114-
} else if len(ret) > 1 {
103+
func handleResponse(fn reflect.Value, ret []reflect.Value) {
104+
if len(ret) != 0 {
115105
panic(fmt.Sprintf("unsupported return values: %s", fn.Type()))
116106
}
117-
return nil
118107
}
119108

120109
func hasResponseBeenWritten(argsIn []reflect.Value) bool {
@@ -171,11 +160,8 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler {
171160
routing.UpdateFuncInfo(req.Context(), funcInfo)
172161
ret := fn.Call(argsIn)
173162

174-
// handle the return value, and defer the cancel function if there is one
175-
cancelFunc := handleResponse(fn, ret)
176-
if cancelFunc != nil {
177-
defer cancelFunc()
178-
}
163+
// handle the return value (no-op at the moment)
164+
handleResponse(fn, ret)
179165

180166
// if the response has not been written, call the next handler
181167
if next != nil && !hasResponseBeenWritten(argsIn) {

modules/web/middleware/data.go

+6-31
Original file line numberDiff line numberDiff line change
@@ -7,46 +7,21 @@ import (
77
"context"
88
"time"
99

10+
"code.gitea.io/gitea/modules/reqctx"
1011
"code.gitea.io/gitea/modules/setting"
1112
)
1213

13-
// ContextDataStore represents a data store
14-
type ContextDataStore interface {
15-
GetData() ContextData
16-
}
17-
18-
type ContextData map[string]any
19-
20-
func (ds ContextData) GetData() ContextData {
21-
return ds
22-
}
23-
24-
func (ds ContextData) MergeFrom(other ContextData) ContextData {
25-
for k, v := range other {
26-
ds[k] = v
27-
}
28-
return ds
29-
}
30-
3114
const ContextDataKeySignedUser = "SignedUser"
3215

33-
type contextDataKeyType struct{}
34-
35-
var contextDataKey contextDataKeyType
36-
37-
func WithContextData(c context.Context) context.Context {
38-
return context.WithValue(c, contextDataKey, make(ContextData, 10))
39-
}
40-
41-
func GetContextData(c context.Context) ContextData {
42-
if ds, ok := c.Value(contextDataKey).(ContextData); ok {
43-
return ds
16+
func GetContextData(c context.Context) reqctx.ContextData {
17+
if rc := reqctx.GetRequestDataStore(c); rc != nil {
18+
return rc.GetData()
4419
}
4520
return nil
4621
}
4722

48-
func CommonTemplateContextData() ContextData {
49-
return ContextData{
23+
func CommonTemplateContextData() reqctx.ContextData {
24+
return reqctx.ContextData{
5025
"IsLandingPageOrganizations": setting.LandingPageURL == setting.LandingPageOrganizations,
5126

5227
"ShowRegistrationButton": setting.Service.ShowRegistrationButton,

modules/web/middleware/flash.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import (
77
"fmt"
88
"html/template"
99
"net/url"
10+
11+
"code.gitea.io/gitea/modules/reqctx"
1012
)
1113

1214
// Flash represents a one time data transfer between two requests.
1315
type Flash struct {
14-
DataStore ContextDataStore
16+
DataStore reqctx.RequestDataStore
1517
url.Values
1618
ErrorMsg, WarningMsg, InfoMsg, SuccessMsg string
1719
}

modules/web/route.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111

1212
"code.gitea.io/gitea/modules/htmlutil"
13+
"code.gitea.io/gitea/modules/reqctx"
1314
"code.gitea.io/gitea/modules/setting"
1415
"code.gitea.io/gitea/modules/web/middleware"
1516

@@ -29,12 +30,12 @@ func Bind[T any](_ T) http.HandlerFunc {
2930
}
3031

3132
// SetForm set the form object
32-
func SetForm(dataStore middleware.ContextDataStore, obj any) {
33+
func SetForm(dataStore reqctx.ContextDataProvider, obj any) {
3334
dataStore.GetData()["__form"] = obj
3435
}
3536

3637
// GetForm returns the validate form information
37-
func GetForm(dataStore middleware.ContextDataStore) any {
38+
func GetForm(dataStore reqctx.RequestDataStore) any {
3839
return dataStore.GetData()["__form"]
3940
}
4041

0 commit comments

Comments
 (0)