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  }