summaryrefslogtreecommitdiffstats
path: root/modules/cache/context.go
diff options
context:
space:
mode:
Diffstat (limited to 'modules/cache/context.go')
-rw-r--r--modules/cache/context.go119
1 files changed, 103 insertions, 16 deletions
diff --git a/modules/cache/context.go b/modules/cache/context.go
index f741a87445..62bbf5dcba 100644
--- a/modules/cache/context.go
+++ b/modules/cache/context.go
@@ -6,6 +6,7 @@ package cache
import (
"context"
"sync"
+ "time"
"code.gitea.io/gitea/modules/log"
)
@@ -14,65 +15,151 @@ import (
// This is useful for caching data that is expensive to calculate and is likely to be
// used multiple times in a request.
type cacheContext struct {
- ctx context.Context
- data map[any]map[any]any
- lock sync.RWMutex
+ data map[any]map[any]any
+ lock sync.RWMutex
+ created time.Time
+ discard bool
}
func (cc *cacheContext) Get(tp, key any) any {
cc.lock.RLock()
defer cc.lock.RUnlock()
- if cc.data[tp] == nil {
- return nil
- }
return cc.data[tp][key]
}
func (cc *cacheContext) Put(tp, key, value any) {
cc.lock.Lock()
defer cc.lock.Unlock()
- if cc.data[tp] == nil {
- cc.data[tp] = make(map[any]any)
+
+ if cc.discard {
+ return
+ }
+
+ d := cc.data[tp]
+ if d == nil {
+ d = make(map[any]any)
+ cc.data[tp] = d
}
- cc.data[tp][key] = value
+ d[key] = value
}
func (cc *cacheContext) Delete(tp, key any) {
cc.lock.Lock()
defer cc.lock.Unlock()
- if cc.data[tp] == nil {
- return
- }
delete(cc.data[tp], key)
}
+func (cc *cacheContext) Discard() {
+ cc.lock.Lock()
+ defer cc.lock.Unlock()
+ cc.data = nil
+ cc.discard = true
+}
+
+func (cc *cacheContext) isDiscard() bool {
+ cc.lock.RLock()
+ defer cc.lock.RUnlock()
+ return cc.discard
+}
+
+// cacheContextLifetime is the max lifetime of cacheContext.
+// Since cacheContext is used to cache data in a request level context, 10s is enough.
+// If a cacheContext is used more than 10s, it's probably misuse.
+const cacheContextLifetime = 10 * time.Second
+
+var timeNow = time.Now
+
+func (cc *cacheContext) Expired() bool {
+ return timeNow().Sub(cc.created) > cacheContextLifetime
+}
+
var cacheContextKey = struct{}{}
+/*
+Since there are both WithCacheContext and WithNoCacheContext,
+it may be confusing when there is nesting.
+
+Some cases to explain the design:
+
+When:
+- A, B or C means a cache context.
+- A', B' or C' means a discard cache context.
+- ctx means context.Backgrand().
+- A(ctx) means a cache context with ctx as the parent context.
+- B(A(ctx)) means a cache context with A(ctx) as the parent context.
+- With is alias of WithCacheContext.
+- WithNo is alias of WithNoCacheContext.
+
+So:
+- With(ctx) -> A(ctx)
+- With(With(ctx)) -> A(ctx), not B(A(ctx)), always reuse parent cache context if possible.
+- With(With(With(ctx))) -> A(ctx), not C(B(A(ctx))), ditto.
+- WithNo(ctx) -> ctx, not A'(ctx), don't create new cache context if we don't have to.
+- WithNo(With(ctx)) -> A'(ctx)
+- WithNo(WithNo(With(ctx))) -> A'(ctx), not B'(A'(ctx)), don't create new cache context if we don't have to.
+- With(WithNo(With(ctx))) -> B(A'(ctx)), not A(ctx), never reuse a discard cache context.
+- WithNo(With(WithNo(With(ctx)))) -> B'(A'(ctx))
+- With(WithNo(With(WithNo(With(ctx))))) -> C(B'(A'(ctx))), so there's always only one not-discard cache context.
+*/
+
func WithCacheContext(ctx context.Context) context.Context {
+ if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
+ if !c.isDiscard() {
+ // reuse parent context
+ return ctx
+ }
+ }
return context.WithValue(ctx, cacheContextKey, &cacheContext{
- ctx: ctx,
- data: make(map[any]map[any]any),
+ data: make(map[any]map[any]any),
+ created: timeNow(),
})
}
+func WithNoCacheContext(ctx context.Context) context.Context {
+ if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
+ // The caller want to run long-life tasks, but the parent context is a cache context.
+ // So we should disable and clean the cache data, or it will be kept in memory for a long time.
+ c.Discard()
+ return ctx
+ }
+
+ return ctx
+}
+
func GetContextData(ctx context.Context, tp, key any) any {
if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
+ if c.Expired() {
+ // The warning means that the cache context is misused for long-life task,
+ // it can be resolved with WithNoCacheContext(ctx).
+ log.Warn("cache context is expired, may be misused for long-life tasks: %v", c)
+ return nil
+ }
return c.Get(tp, key)
}
- log.Warn("cannot get cache context when getting data: %v", ctx)
return nil
}
func SetContextData(ctx context.Context, tp, key, value any) {
if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
+ if c.Expired() {
+ // The warning means that the cache context is misused for long-life task,
+ // it can be resolved with WithNoCacheContext(ctx).
+ log.Warn("cache context is expired, may be misused for long-life tasks: %v", c)
+ return
+ }
c.Put(tp, key, value)
return
}
- log.Warn("cannot get cache context when setting data: %v", ctx)
}
func RemoveContextData(ctx context.Context, tp, key any) {
if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
+ if c.Expired() {
+ // The warning means that the cache context is misused for long-life task,
+ // it can be resolved with WithNoCacheContext(ctx).
+ log.Warn("cache context is expired, may be misused for long-life tasks: %v", c)
+ return
+ }
c.Delete(tp, key)
}
}