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  }