code.gitea.io/gitea@v1.22.3/modules/cache/context.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package cache
     5  
     6  import (
     7  	"context"
     8  	"sync"
     9  	"time"
    10  
    11  	"code.gitea.io/gitea/modules/log"
    12  )
    13  
    14  // cacheContext is a context that can be used to cache data in a request level context
    15  // This is useful for caching data that is expensive to calculate and is likely to be
    16  // used multiple times in a request.
    17  type cacheContext struct {
    18  	data    map[any]map[any]any
    19  	lock    sync.RWMutex
    20  	created time.Time
    21  	discard bool
    22  }
    23  
    24  func (cc *cacheContext) Get(tp, key any) any {
    25  	cc.lock.RLock()
    26  	defer cc.lock.RUnlock()
    27  	return cc.data[tp][key]
    28  }
    29  
    30  func (cc *cacheContext) Put(tp, key, value any) {
    31  	cc.lock.Lock()
    32  	defer cc.lock.Unlock()
    33  
    34  	if cc.discard {
    35  		return
    36  	}
    37  
    38  	d := cc.data[tp]
    39  	if d == nil {
    40  		d = make(map[any]any)
    41  		cc.data[tp] = d
    42  	}
    43  	d[key] = value
    44  }
    45  
    46  func (cc *cacheContext) Delete(tp, key any) {
    47  	cc.lock.Lock()
    48  	defer cc.lock.Unlock()
    49  	delete(cc.data[tp], key)
    50  }
    51  
    52  func (cc *cacheContext) Discard() {
    53  	cc.lock.Lock()
    54  	defer cc.lock.Unlock()
    55  	cc.data = nil
    56  	cc.discard = true
    57  }
    58  
    59  func (cc *cacheContext) isDiscard() bool {
    60  	cc.lock.RLock()
    61  	defer cc.lock.RUnlock()
    62  	return cc.discard
    63  }
    64  
    65  // cacheContextLifetime is the max lifetime of cacheContext.
    66  // Since cacheContext is used to cache data in a request level context, 5 minutes is enough.
    67  // If a cacheContext is used more than 5 minutes, it's probably misuse.
    68  const cacheContextLifetime = 5 * time.Minute
    69  
    70  var timeNow = time.Now
    71  
    72  func (cc *cacheContext) Expired() bool {
    73  	return timeNow().Sub(cc.created) > cacheContextLifetime
    74  }
    75  
    76  var cacheContextKey = struct{}{}
    77  
    78  /*
    79  Since there are both WithCacheContext and WithNoCacheContext,
    80  it may be confusing when there is nesting.
    81  
    82  Some cases to explain the design:
    83  
    84  When:
    85  - A, B or C means a cache context.
    86  - A', B' or C' means a discard cache context.
    87  - ctx means context.Backgrand().
    88  - A(ctx) means a cache context with ctx as the parent context.
    89  - B(A(ctx)) means a cache context with A(ctx) as the parent context.
    90  - With is alias of WithCacheContext.
    91  - WithNo is alias of WithNoCacheContext.
    92  
    93  So:
    94  - With(ctx) -> A(ctx)
    95  - With(With(ctx)) -> A(ctx), not B(A(ctx)), always reuse parent cache context if possible.
    96  - With(With(With(ctx))) -> A(ctx), not C(B(A(ctx))), ditto.
    97  - WithNo(ctx) -> ctx, not A'(ctx), don't create new cache context if we don't have to.
    98  - WithNo(With(ctx)) -> A'(ctx)
    99  - WithNo(WithNo(With(ctx))) -> A'(ctx), not B'(A'(ctx)), don't create new cache context if we don't have to.
   100  - With(WithNo(With(ctx))) -> B(A'(ctx)), not A(ctx), never reuse a discard cache context.
   101  - WithNo(With(WithNo(With(ctx)))) -> B'(A'(ctx))
   102  - With(WithNo(With(WithNo(With(ctx))))) -> C(B'(A'(ctx))), so there's always only one not-discard cache context.
   103  */
   104  
   105  func WithCacheContext(ctx context.Context) context.Context {
   106  	if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
   107  		if !c.isDiscard() {
   108  			// reuse parent context
   109  			return ctx
   110  		}
   111  	}
   112  	return context.WithValue(ctx, cacheContextKey, &cacheContext{
   113  		data:    make(map[any]map[any]any),
   114  		created: timeNow(),
   115  	})
   116  }
   117  
   118  func WithNoCacheContext(ctx context.Context) context.Context {
   119  	if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
   120  		// The caller want to run long-life tasks, but the parent context is a cache context.
   121  		// So we should disable and clean the cache data, or it will be kept in memory for a long time.
   122  		c.Discard()
   123  		return ctx
   124  	}
   125  
   126  	return ctx
   127  }
   128  
   129  func GetContextData(ctx context.Context, tp, key any) any {
   130  	if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
   131  		if c.Expired() {
   132  			// The warning means that the cache context is misused for long-life task,
   133  			// it can be resolved with WithNoCacheContext(ctx).
   134  			log.Warn("cache context is expired, is highly likely to be misused for long-life tasks: %v", c)
   135  			return nil
   136  		}
   137  		return c.Get(tp, key)
   138  	}
   139  	return nil
   140  }
   141  
   142  func SetContextData(ctx context.Context, tp, key, value any) {
   143  	if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
   144  		if c.Expired() {
   145  			// The warning means that the cache context is misused for long-life task,
   146  			// it can be resolved with WithNoCacheContext(ctx).
   147  			log.Warn("cache context is expired, is highly likely to be misused for long-life tasks: %v", c)
   148  			return
   149  		}
   150  		c.Put(tp, key, value)
   151  		return
   152  	}
   153  }
   154  
   155  func RemoveContextData(ctx context.Context, tp, key any) {
   156  	if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
   157  		if c.Expired() {
   158  			// The warning means that the cache context is misused for long-life task,
   159  			// it can be resolved with WithNoCacheContext(ctx).
   160  			log.Warn("cache context is expired, is highly likely to be misused for long-life tasks: %v", c)
   161  			return
   162  		}
   163  		c.Delete(tp, key)
   164  	}
   165  }
   166  
   167  // GetWithContextCache returns the cache value of the given key in the given context.
   168  func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error) {
   169  	v := GetContextData(ctx, cacheGroupKey, cacheTargetID)
   170  	if vv, ok := v.(T); ok {
   171  		return vv, nil
   172  	}
   173  	t, err := f()
   174  	if err != nil {
   175  		return t, err
   176  	}
   177  	SetContextData(ctx, cacheGroupKey, cacheTargetID, t)
   178  	return t, nil
   179  }