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 }