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