github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/api/query/cache/service/main.go (about) 1 // Copyright 2018 The WPT Dashboard Project. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package main 6 7 import ( 8 "context" 9 "flag" 10 "fmt" 11 "net/http" 12 "os" 13 "runtime" 14 "syscall" 15 "time" 16 17 "cloud.google.com/go/datastore" 18 "github.com/sirupsen/logrus" 19 "github.com/web-platform-tests/wpt.fyi/api/query/cache/backfill" 20 "github.com/web-platform-tests/wpt.fyi/api/query/cache/index" 21 "github.com/web-platform-tests/wpt.fyi/api/query/cache/monitor" 22 "github.com/web-platform-tests/wpt.fyi/api/query/cache/poll" 23 "github.com/web-platform-tests/wpt.fyi/shared" 24 "google.golang.org/api/option" 25 ) 26 27 // nolint:gochecknoglobals // TODO: Fix gochecknoglobals lint error 28 var ( 29 port = flag.Int("port", 8080, "Port to listen on") 30 projectID = flag.String("project_id", "", 31 "Google Cloud Platform project ID, used for connecting to Datastore") 32 gcpCredentialsFile = flag.String("gcp_credentials_file", "", 33 "Path to Google Cloud Platform credentials file, if necessary") 34 numShards = flag.Int("num_shards", runtime.NumCPU(), "Number of shards for parallelizing query execution") 35 monitorInterval = flag.Duration("monitor_interval", time.Second*5, "Polling interval for memory usage monitor") 36 monitorMaxIngestedRuns = flag.Uint("monitor_max_ingested_runs", 10, 37 "Maximum number of runs that can be ingested before memory monitor must run") 38 maxHeapBytes = flag.Uint64("max_heap_bytes", 0, 39 "Soft limit on heap-allocated bytes before evicting test runs from memory") 40 evictRunsPercent = flag.Float64("evict_runs_percent", 0.1, 41 "Decimal percentage indicating what fraction of runs to evict when soft memory limit is reached") 42 updateInterval = flag.Duration("update_interval", time.Second*10, 43 "Update interval for polling for new runs") 44 updateMaxRuns = flag.Int("update_max_runs", 10, 45 "The maximum number of latest runs to lookup in attempts to update indexes via polling") 46 maxRunsPerRequest = flag.Int("max_runs_per_request", 16, 47 "Maximum number of runs that may be queried per request") 48 49 // User-facing message for when runs in a request exceeds maxRunsPerRequest. 50 // Set in init() after parsing flags. 51 maxRunsPerRequestMsg string 52 53 idx index.Index 54 mon monitor.Monitor 55 ) 56 57 func livenessCheckHandler(w http.ResponseWriter, r *http.Request) { 58 _, err := w.Write([]byte("Alive")) 59 if err != nil { 60 logger := shared.GetLogger(r.Context()) 61 logger.Warningf("Failed to write data in liveness check handler: %s", err.Error()) 62 } 63 } 64 65 func readinessCheckHandler(w http.ResponseWriter, r *http.Request) { 66 if idx == nil || mon == nil { 67 http.Error(w, "Cache not yet ready", http.StatusServiceUnavailable) 68 69 return 70 } 71 72 _, err := w.Write([]byte("Ready")) 73 if err != nil { 74 logger := shared.GetLogger(r.Context()) 75 logger.Warningf("Failed to write data in readiness check handler: %s", err.Error()) 76 } 77 } 78 79 func searchHandler(w http.ResponseWriter, r *http.Request) { 80 err := searchHandlerImpl(w, r) 81 if err != nil { 82 log := shared.GetLogger(r.Context()) 83 log.Errorf(err.Error()) 84 http.Error(w, err.Message, err.Code) 85 } 86 } 87 88 // nolint:ireturn // TODO: Fix ireturn lint error 89 func getDatastore(ctx context.Context) (shared.Datastore, error) { 90 var client *datastore.Client 91 var err error 92 if gcpCredentialsFile != nil && *gcpCredentialsFile != "" { 93 client, err = datastore.NewClient(ctx, *projectID, option.WithCredentialsFile(*gcpCredentialsFile)) 94 } else { 95 client, err = datastore.NewClient(ctx, *projectID) 96 } 97 if err != nil { 98 return nil, err 99 } 100 d := shared.NewCloudDatastore(ctx, client) 101 102 return d, nil 103 } 104 105 func init() { 106 flag.Parse() 107 108 if *maxHeapBytes == 0 { 109 var sysinfo syscall.Sysinfo_t 110 if err := syscall.Sysinfo(&sysinfo); err != nil { 111 logrus.Fatalf("Unable to get total system memory: %s", err.Error()) 112 } 113 sysmem := float64(sysinfo.Totalram) * float64(sysinfo.Unit) 114 // Reserve 2GB or 50% of the total memory for system (whichever is smaller). 115 if sysmem-2e9 > sysmem*0.5 { 116 *maxHeapBytes = uint64(sysmem - 2e9) 117 } else { 118 *maxHeapBytes = uint64(sysmem * 0.5) 119 } 120 logrus.Infof("Detected total system memory: %d; setting max heap size to %d", uint64(sysmem), *maxHeapBytes) 121 } 122 123 maxRunsPerRequestMsg = fmt.Sprintf("Too many runs specified; maximum is %d.", *maxRunsPerRequest) 124 125 autoProjectID := os.Getenv("GOOGLE_CLOUD_PROJECT") 126 if autoProjectID == "" { 127 logrus.Warningf("Failed to get project ID from environment") 128 } else { 129 if *projectID == "" { 130 logrus.Infof("Using project ID from environment: %s", autoProjectID) 131 *projectID = autoProjectID 132 } else if *projectID != autoProjectID { 133 logrus.Warningf( 134 "Using project ID from flag: %s, even though environment reports project ID: %s", 135 *projectID, 136 autoProjectID, 137 ) 138 } else { 139 logrus.Infof("Using project ID: %s", *projectID) 140 } 141 } 142 } 143 144 func main() { 145 logrus.Infof("Serving index with %d shards", *numShards) 146 // nolint:godox // TODO: Use different field configurations for index, backfiller, monitor? 147 logger := logrus.StandardLogger() 148 149 var err error 150 idx, err = index.NewShardedWPTIndex(index.HTTPReportLoader{}, *numShards) 151 if err != nil { 152 logrus.Fatalf("Failed to instantiate index: %v", err) 153 } 154 155 store, err := backfill.GetDatastore(*projectID, gcpCredentialsFile, logger) 156 if err != nil { 157 logrus.Fatalf("Failed to get datastore: %s", err) 158 } 159 mon, err = backfill.FillIndex( 160 store, 161 logger, 162 monitor.GoRuntime{}, 163 *monitorInterval, 164 *monitorMaxIngestedRuns, 165 *maxHeapBytes, 166 *evictRunsPercent, 167 idx, 168 ) 169 if err != nil { 170 logrus.Fatalf("Failed to initiate index backkfill: %v", err) 171 } 172 173 // Index, backfiller, monitor now in place. Start polling to load runs added 174 // after backfilling was started. 175 go poll.KeepRunsUpdated(store, logger, *updateInterval, *updateMaxRuns, idx) 176 177 // Initializes clients. 178 if err = shared.Clients.Init(context.Background()); err != nil { 179 logrus.Fatalf("Failed to initialize Google Cloud clients: %v", err) 180 } 181 defer shared.Clients.Close() 182 183 // Polls Metadata update every 10 minutes. 184 go poll.StartMetadataPollingService(context.Background(), logger, time.Minute*10) 185 186 // Polls Web Feature Manifest update every 30 minutes. 187 go poll.StartWebFeaturesManifestPollingService(context.Background(), logger, time.Minute*30) 188 189 http.HandleFunc("/_ah/liveness_check", livenessCheckHandler) 190 http.HandleFunc("/_ah/readiness_check", readinessCheckHandler) 191 http.HandleFunc("/api/search/cache", shared.HandleWithLogging(searchHandler)) 192 logrus.Infof("Listening on port %d", *port) 193 // nolint:gosec // TODO: Fix gosec lint error (G114). 194 logrus.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil)) 195 }