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  }