github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/cache.go (about) 1 // Copyright 2020 syzkaller project authors. All rights reserved. 2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4 package main 5 6 import ( 7 "context" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "net/http" 12 "sort" 13 "time" 14 15 "github.com/google/syzkaller/pkg/hash" 16 "github.com/google/syzkaller/pkg/image" 17 "google.golang.org/appengine/v2" 18 "google.golang.org/appengine/v2/log" 19 "google.golang.org/appengine/v2/memcache" 20 ) 21 22 type Cached struct { 23 MissingBackports int 24 Total CachedBugStats 25 Subsystems map[string]CachedBugStats 26 NoSubsystem CachedBugStats 27 } 28 29 type CachedBugStats struct { 30 Open int 31 Fixed int 32 Invalid int 33 } 34 35 func CacheGet(c context.Context, r *http.Request, ns string) (*Cached, error) { 36 accessLevel := accessLevel(c, r) 37 v := new(Cached) 38 _, err := memcache.Gob.Get(c, cacheKey(ns, accessLevel), v) 39 if err != nil && err != memcache.ErrCacheMiss { 40 return nil, err 41 } 42 if err == nil { 43 return v, nil 44 } 45 bugs, _, err := loadNamespaceBugs(c, ns) 46 if err != nil { 47 return nil, err 48 } 49 backports, err := loadAllBackports(c) 50 if err != nil { 51 return nil, err 52 } 53 return buildAndStoreCached(c, bugs, backports, ns, accessLevel) 54 } 55 56 var cacheAccessLevels = []AccessLevel{AccessPublic, AccessUser, AccessAdmin} 57 58 // cacheUpdate updates memcache every hour (called by cron.yaml). 59 // Cache update is slow and we don't want to slow down user requests. 60 func cacheUpdate(w http.ResponseWriter, r *http.Request) { 61 c := appengine.NewContext(r) 62 backports, err := loadAllBackports(c) 63 if err != nil { 64 log.Errorf(c, "failed load backports: %v", err) 65 return 66 } 67 for ns := range getConfig(c).Namespaces { 68 bugs, _, err := loadNamespaceBugs(c, ns) 69 if err != nil { 70 log.Errorf(c, "failed load ns=%v bugs: %v", ns, err) 71 continue 72 } 73 for _, accessLevel := range cacheAccessLevels { 74 _, err := buildAndStoreCached(c, bugs, backports, ns, accessLevel) 75 if err != nil { 76 log.Errorf(c, "failed to build cached for ns=%v access=%v: %v", ns, accessLevel, err) 77 continue 78 } 79 } 80 } 81 } 82 83 func buildAndStoreCached(c context.Context, bugs []*Bug, backports []*rawBackport, 84 ns string, accessLevel AccessLevel) (*Cached, error) { 85 v := &Cached{ 86 Subsystems: make(map[string]CachedBugStats), 87 } 88 for _, bug := range bugs { 89 if bug.Status == BugStatusOpen && accessLevel < bug.sanitizeAccess(c, accessLevel) { 90 continue 91 } 92 v.Total.Record(bug) 93 subsystems := bug.LabelValues(SubsystemLabel) 94 for _, label := range subsystems { 95 stats := v.Subsystems[label.Value] 96 stats.Record(bug) 97 v.Subsystems[label.Value] = stats 98 } 99 if len(subsystems) == 0 { 100 v.NoSubsystem.Record(bug) 101 } 102 } 103 for _, backport := range backports { 104 outgoing := stringInList(backport.FromNs, ns) 105 for _, bug := range backport.Bugs { 106 if accessLevel < bug.sanitizeAccess(c, accessLevel) { 107 continue 108 } 109 if bug.Namespace == ns || outgoing { 110 v.MissingBackports++ 111 } 112 } 113 } 114 115 item := &memcache.Item{ 116 Key: cacheKey(ns, accessLevel), 117 Object: v, 118 Expiration: 4 * time.Hour, // supposed to be updated by cron every hour 119 } 120 if err := memcache.Gob.Set(c, item); err != nil { 121 return nil, err 122 } 123 return v, nil 124 } 125 126 func (c *CachedBugStats) Record(bug *Bug) { 127 switch bug.Status { 128 case BugStatusOpen: 129 if len(bug.Commits) == 0 { 130 c.Open++ 131 } else { 132 c.Fixed++ 133 } 134 case BugStatusFixed: 135 c.Fixed++ 136 case BugStatusInvalid: 137 c.Invalid++ 138 } 139 } 140 141 func cacheKey(ns string, accessLevel AccessLevel) string { 142 return fmt.Sprintf("%v-%v", ns, accessLevel) 143 } 144 145 func CachedBugGroups(c context.Context, ns string, accessLevel AccessLevel) ([]*uiBugGroup, error) { 146 item, err := memcache.Get(c, cachedBugGroupsKey(ns, accessLevel)) 147 if err == memcache.ErrCacheMiss { 148 return nil, nil 149 } 150 if err != nil { 151 return nil, err 152 } 153 154 jsonData, destructor := image.MustDecompress(item.Value) 155 defer destructor() 156 157 var ret []*uiBugGroup 158 err = json.Unmarshal(jsonData, &ret) 159 return ret, err 160 } 161 162 func cachedBugGroupsKey(ns string, accessLevel AccessLevel) string { 163 return fmt.Sprintf("%v-%v-bug-groups", ns, accessLevel) 164 } 165 166 // minuteCacheUpdate updates memcache every minute (called by cron.yaml). 167 func handleMinuteCacheUpdate(w http.ResponseWriter, r *http.Request) { 168 c := appengine.NewContext(r) 169 for ns, nsConfig := range getConfig(c).Namespaces { 170 if !nsConfig.CacheUIPages { 171 continue 172 } 173 err := minuteCacheNsUpdate(c, ns) 174 if err != nil { 175 http.Error(w, fmt.Sprintf("bug groups cache update for %s failed: %v", ns, err), 176 http.StatusInternalServerError) 177 return 178 } 179 } 180 } 181 182 func minuteCacheNsUpdate(c context.Context, ns string) error { 183 bugs, err := loadVisibleBugs(c, ns, nil) 184 if err != nil { 185 return err 186 } 187 managers, err := managerList(c, ns) 188 if err != nil { 189 return err 190 } 191 for _, accessLevel := range cacheAccessLevels { 192 groups, err := prepareBugGroups(c, bugs, managers, accessLevel, ns) 193 if err != nil { 194 return fmt.Errorf("failed to fetch groups: %w", err) 195 } 196 encoded, err := json.Marshal(groups) 197 if err != nil { 198 return fmt.Errorf("failed to marshal: %w", err) 199 } 200 item := &memcache.Item{ 201 Key: cachedBugGroupsKey(ns, accessLevel), 202 // The resulting blob can be quite big, so let's compress. 203 Value: image.Compress(encoded), 204 Expiration: 2 * time.Minute, // supposed to be updated by cron every minute 205 } 206 if err := memcache.Set(c, item); err != nil { 207 return err 208 } 209 } 210 return nil 211 } 212 213 func CachedManagerList(c context.Context, ns string) ([]string, error) { 214 return cachedObjectList(c, 215 fmt.Sprintf("%s-managers-list", ns), 216 time.Minute, 217 func(c context.Context) ([]string, error) { 218 return managerList(c, ns) 219 }, 220 ) 221 } 222 223 func CachedUIManagers(c context.Context, accessLevel AccessLevel, ns string, 224 filter *userBugFilter) ([]*uiManager, error) { 225 return cachedObjectList(c, 226 fmt.Sprintf("%s-%v-%v-ui-managers", ns, accessLevel, filter.Hash()), 227 5*time.Minute, 228 func(c context.Context) ([]*uiManager, error) { 229 return loadManagers(c, accessLevel, ns, filter) 230 }, 231 ) 232 } 233 234 func cachedObjectList[T any](c context.Context, key string, period time.Duration, 235 load func(context.Context) ([]T, error)) ([]T, error) { 236 // Check if the object is in cache. 237 var obj []T 238 _, err := memcache.Gob.Get(c, key, &obj) 239 if err == nil { 240 return obj, nil 241 } else if err != memcache.ErrCacheMiss { 242 return nil, err 243 } 244 245 // Load the object. 246 obj, err = load(c) 247 if err != nil { 248 return nil, err 249 } 250 item := &memcache.Item{ 251 Key: key, 252 Object: obj, 253 Expiration: period, 254 } 255 if err := memcache.Gob.Set(c, item); err != nil { 256 return nil, err 257 } 258 return obj, nil 259 } 260 261 type RequesterInfo struct { 262 Requests []time.Time 263 } 264 265 func (ri *RequesterInfo) Record(now time.Time, cfg ThrottleConfig) bool { 266 var newRequests []time.Time 267 for _, req := range ri.Requests { 268 if now.Sub(req) >= cfg.Window { 269 continue 270 } 271 newRequests = append(newRequests, req) 272 } 273 newRequests = append(newRequests, now) 274 sort.Slice(ri.Requests, func(i, j int) bool { return ri.Requests[i].Before(ri.Requests[j]) }) 275 // Don't store more than needed. 276 if len(newRequests) > cfg.Limit+1 { 277 newRequests = newRequests[len(newRequests)-(cfg.Limit+1):] 278 } 279 ri.Requests = newRequests 280 // Check that we satisfy the conditions. 281 return len(newRequests) <= cfg.Limit 282 } 283 284 var ErrThrottleTooManyRetries = errors.New("all attempts to record request failed") 285 286 func ThrottleRequest(c context.Context, requesterID string) (bool, error) { 287 cfg := getConfig(c).Throttle 288 if cfg.Empty() || requesterID == "" { 289 // No sense to query memcached. 290 return true, nil 291 } 292 key := fmt.Sprintf("requester-%s", hash.String([]byte(requesterID))) 293 const attempts = 5 294 for i := 0; i < attempts; i++ { 295 var obj RequesterInfo 296 item, err := memcache.Gob.Get(c, key, &obj) 297 if err == memcache.ErrCacheMiss { 298 ok := obj.Record(timeNow(c), cfg) 299 err = memcache.Gob.Add(c, &memcache.Item{ 300 Key: key, 301 Object: obj, 302 Expiration: cfg.Window, 303 }) 304 if err == memcache.ErrNotStored { 305 // Conflict with another instance. Retry. 306 continue 307 } 308 return ok, err 309 } else if err != nil { 310 return false, err 311 } 312 // Update the existing object. 313 ok := obj.Record(timeNow(c), cfg) 314 item.Expiration = cfg.Window 315 item.Object = obj 316 err = memcache.Gob.CompareAndSwap(c, item) 317 if err == memcache.ErrCASConflict { 318 if ok { 319 // Only retry if we approved the query. 320 // If we denied and there was a concurrent write 321 // to the same object, it could have only denied 322 // the query as well. 323 // Our save won't change anything. 324 continue 325 } 326 } else if err != nil { 327 return false, err 328 } 329 return ok, nil 330 } 331 return false, ErrThrottleTooManyRetries 332 }