123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179 |
- // Copyright 2022 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package cache
-
- import (
- "context"
- "sync"
- "time"
-
- "code.gitea.io/gitea/modules/log"
- )
-
- // cacheContext is a context that can be used to cache data in a request level context
- // 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 {
- 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()
- return cc.data[tp][key]
- }
-
- func (cc *cacheContext) Put(tp, key, value any) {
- cc.lock.Lock()
- defer cc.lock.Unlock()
-
- if cc.discard {
- return
- }
-
- d := cc.data[tp]
- if d == nil {
- d = make(map[any]any)
- cc.data[tp] = d
- }
- d[key] = value
- }
-
- func (cc *cacheContext) Delete(tp, key any) {
- cc.lock.Lock()
- defer cc.lock.Unlock()
- 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{
- 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)
- }
- 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
- }
- }
-
- 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)
- }
- }
-
- // GetWithContextCache returns the cache value of the given key in the given context.
- func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error) {
- v := GetContextData(ctx, cacheGroupKey, cacheTargetID)
- if vv, ok := v.(T); ok {
- return vv, nil
- }
- t, err := f()
- if err != nil {
- return t, err
- }
- SetContextData(ctx, cacheGroupKey, cacheTargetID, t)
- return t, nil
- }
|