k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/greenhouse/main.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // greenhouse implements a bazel remote cache service [1] 18 // supporting arbitrarily many workspaces stored within the same 19 // top level directory. 20 // 21 // the first path segment in each {PUT,GET} request is mapped to an individual 22 // workspace cache, the remaining segments should follow [2]. 23 // 24 // nursery assumes you are using SHA256 25 // 26 // [1] https://docs.bazel.build/versions/master/remote-caching.html 27 // [2] https://docs.bazel.build/versions/master/remote-caching.html#http-caching-protocol 28 package main 29 30 import ( 31 "errors" 32 "flag" 33 "fmt" 34 "io" 35 "net/http" 36 "os" 37 "strings" 38 "time" 39 40 "k8s.io/test-infra/greenhouse/diskcache" 41 "k8s.io/test-infra/greenhouse/diskutil" 42 "sigs.k8s.io/prow/pkg/logrusutil" 43 44 "github.com/prometheus/client_golang/prometheus/promhttp" 45 "github.com/sirupsen/logrus" 46 ) 47 48 var dir = flag.String("dir", "", "location to store cache entries on disk") 49 var host = flag.String("host", "", "host address to listen on") 50 var cachePort = flag.Int("cache-port", 8080, "port to listen on for cache requests") 51 var metricsPort = flag.Int("metrics-port", 9090, "port to listen on for prometheus metrics scraping") 52 var metricsUpdateInterval = flag.Duration("metrics-update-interval", time.Second*10, 53 "interval between updating disk metrics") 54 55 // eviction knobs 56 var minPercentBlocksFree = flag.Float64("min-percent-blocks-free", 5, 57 "minimum percent of blocks free on --dir's disk before evicting entries") 58 var evictUntilPercentBlocksFree = flag.Float64("evict-until-percent-blocks-free", 20, 59 "continue evicting from the cache until at least this percent of blocks are free") 60 var diskCheckInterval = flag.Duration("disk-check-interval", time.Second*10, 61 "interval between checking disk usage (and potentially evicting entries)") 62 63 // global metrics object, see prometheus.go 64 var promMetrics *prometheusMetrics 65 66 func init() { 67 logrusutil.ComponentInit() 68 69 logrus.SetOutput(os.Stdout) 70 promMetrics = initMetrics() 71 } 72 73 func main() { 74 flag.Parse() 75 if *dir == "" { 76 logrus.Fatal("--dir must be set!") 77 } 78 79 cache := diskcache.NewCache(*dir) 80 go monitorDiskAndEvict( 81 cache, *diskCheckInterval, 82 *minPercentBlocksFree, *evictUntilPercentBlocksFree, 83 ) 84 85 go updateMetrics(*metricsUpdateInterval, cache.DiskRoot()) 86 87 // listen for prometheus scraping 88 metricsMux := http.NewServeMux() 89 metricsMux.Handle("/prometheus", promhttp.Handler()) 90 metricsAddr := fmt.Sprintf("%s:%d", *host, *metricsPort) 91 go func() { 92 logrus.Infof("Metrics Listening on: %s", metricsAddr) 93 logrus.WithField("mux", "metrics").WithError( 94 http.ListenAndServe(metricsAddr, metricsMux), 95 ).Fatal("ListenAndServe returned.") 96 }() 97 98 // listen for cache requests 99 cacheMux := http.NewServeMux() 100 cacheMux.Handle("/", cacheHandler(cache)) 101 cacheAddr := fmt.Sprintf("%s:%d", *host, *cachePort) 102 logrus.Infof("Cache Listening on: %s", cacheAddr) 103 logrus.WithField("mux", "cache").WithError( 104 http.ListenAndServe(cacheAddr, cacheMux), 105 ).Fatal("ListenAndServe returned.") 106 } 107 108 // file not found error, used below 109 var errNotFound = errors.New("entry not found") 110 111 func cacheHandler(cache *diskcache.Cache) http.Handler { 112 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 113 logger := logrus.WithFields(logrus.Fields{ 114 "method": r.Method, 115 "path": r.URL.Path, 116 }) 117 // parse and validate path 118 // the last segment should be a hash, and 119 // the second to last segment should be "ac" or "cas" 120 parts := strings.Split(r.URL.Path, "/") 121 if len(parts) < 3 { 122 logger.Warn("received an invalid request") 123 http.Error(w, "invalid location", http.StatusBadRequest) 124 return 125 } 126 hash := parts[len(parts)-1] 127 acOrCAS := parts[len(parts)-2] 128 if acOrCAS != "ac" && acOrCAS != "cas" { 129 logger.Warn("received an invalid request at path") 130 http.Error(w, "invalid location", http.StatusBadRequest) 131 return 132 } 133 requestingAction := acOrCAS == "ac" 134 135 // actually handle request depending on method 136 switch m := r.Method; m { 137 // handle retrieval 138 case http.MethodGet: 139 err := cache.Get(r.URL.Path, func(exists bool, contents io.ReadSeeker) error { 140 if !exists { 141 return errNotFound 142 } 143 http.ServeContent(w, r, "", time.Time{}, contents) 144 return nil 145 }) 146 if err != nil { 147 // file not present 148 if err == errNotFound { 149 if requestingAction { 150 promMetrics.ActionCacheMisses.Inc() 151 } else { 152 promMetrics.CASMisses.Inc() 153 } 154 http.Error(w, err.Error(), http.StatusNotFound) 155 return 156 } 157 // unknown error 158 logger.WithError(err).Error("error getting key") 159 http.Error(w, err.Error(), http.StatusInternalServerError) 160 return 161 } 162 // success, log hit 163 if requestingAction { 164 promMetrics.ActionCacheHits.Inc() 165 } else { 166 promMetrics.CASHits.Inc() 167 } 168 169 // handle upload 170 case http.MethodPut: 171 // only hash CAS, not action cache 172 // the action cache is hash -> metadata 173 // the CAS is well, a CAS, which we can hash... 174 if requestingAction { 175 hash = "" 176 } 177 err := cache.Put(r.URL.Path, r.Body, hash) 178 if err != nil { 179 logger.WithError(err).Errorf("Failed to put: %v", r.URL.Path) 180 http.Error(w, "failed to put in cache", http.StatusInternalServerError) 181 return 182 } 183 184 // handle unsupported methods... 185 default: 186 logger.Warn("received an invalid request method") 187 http.Error(w, "unsupported method", http.StatusMethodNotAllowed) 188 } 189 }) 190 } 191 192 // helper to update disk metrics 193 func updateMetrics(interval time.Duration, diskRoot string) { 194 logger := logrus.WithField("sync-loop", "updateMetrics") 195 ticker := time.NewTicker(interval) 196 for ; true; <-ticker.C { 197 logger.Info("tick") 198 _, bytesFree, bytesUsed, _, _, _, err := diskutil.GetDiskUsage(diskRoot) 199 if err != nil { 200 logger.WithError(err).Error("Failed to get disk metrics") 201 } else { 202 promMetrics.DiskFree.Set(float64(bytesFree) / 1e9) 203 promMetrics.DiskUsed.Set(float64(bytesUsed) / 1e9) 204 promMetrics.DiskTotal.Set(float64(bytesFree+bytesUsed) / 1e9) 205 } 206 } 207 }