github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/api/query/cache/poll/poll.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 poll
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"net/http"
    11  	"strconv"
    12  	"time"
    13  
    14  	"github.com/google/go-github/v47/github"
    15  	"github.com/web-platform-tests/wpt.fyi/api/query"
    16  	"github.com/web-platform-tests/wpt.fyi/api/query/cache/index"
    17  	"github.com/web-platform-tests/wpt.fyi/shared"
    18  )
    19  
    20  // KeepRunsUpdated implements updates to an index.Index via simple polling every
    21  // interval duration for at most limit runs loaded from fetcher.
    22  // nolint:gocognit // TODO: Fix gocognit lint error
    23  func KeepRunsUpdated(store shared.Datastore, logger shared.Logger, interval time.Duration, limit int, idx index.Index) {
    24  	// Start by waiting polling interval. This reduces the chance of false alarms
    25  	// from log monitoring when KeepRunsUpdated is invoked around the same time as
    26  	// index backfilling.
    27  	logger.Infof("Starting index update via polling; waiting polling interval first...")
    28  	time.Sleep(interval)
    29  	logger.Infof("Index update via polling started")
    30  
    31  	lastLoadTime := time.Now()
    32  	for {
    33  		start := time.Now()
    34  
    35  		runs, err := store.TestRunQuery().LoadTestRuns(shared.GetDefaultProducts(), nil, nil, nil, nil, &limit, nil)
    36  		if err != nil {
    37  			logger.Errorf("Error fetching runs for update: %v", err)
    38  			wait(start, interval)
    39  
    40  			continue
    41  		}
    42  		if len(runs) == 0 {
    43  			logger.Errorf("Fetcher produced no runs for update")
    44  			wait(start, interval)
    45  
    46  			continue
    47  		}
    48  
    49  		errs := make([]error, len(runs))
    50  		found := false
    51  		for i, browserRuns := range runs {
    52  			for _, run := range browserRuns.TestRuns {
    53  				err := idx.IngestRun(run)
    54  				errs[i] = err
    55  				if err != nil {
    56  					if errors.Is(err, index.ErrRunExists()) {
    57  						logger.Debugf("Not updating run (already exists): %v", run)
    58  					} else if errors.Is(err, index.ErrRunLoading()) {
    59  						logger.Debugf("Not updating run (already loading): %v", run)
    60  					} else {
    61  						logger.Errorf("Error ingesting run: %v: %v", run, err)
    62  					}
    63  				} else {
    64  					logger.Debugf("Updated run index; new run: %v", run)
    65  					found = true
    66  					lastLoadTime = time.Now()
    67  				}
    68  			}
    69  		}
    70  
    71  		if !found {
    72  			logger.Infof("No runs loaded throughout polling iteration. Last run update was at %v", lastLoadTime)
    73  		} else {
    74  			next := errs[1:]
    75  			for i := range next {
    76  				if errs[i] != nil && next[i] == nil {
    77  					logger.Errorf("Ingested run after skipping %d runs; ingest run attempt errors: %v", i, errs)
    78  
    79  					break
    80  				}
    81  			}
    82  		}
    83  
    84  		wait(start, interval)
    85  	}
    86  }
    87  
    88  func wait(start time.Time, total time.Duration) {
    89  	t := total - time.Since(start)
    90  	if t > 0 {
    91  		time.Sleep(t)
    92  	}
    93  }
    94  
    95  // StartMetadataPollingService performs metadata-related services via simple polling every
    96  // interval duration.
    97  func StartMetadataPollingService(ctx context.Context, logger shared.Logger, interval time.Duration) {
    98  	logger.Infof("Starting Metadata polling service.")
    99  	toBeRemovedPRs := make([]string, 0)
   100  	netClient := &http.Client{Timeout: time.Second * 5}
   101  	cacheSet := shared.NewRedisSet()
   102  	gitHubClient, err := shared.NewAppEngineAPI(ctx).GetGitHubClient()
   103  	if err != nil {
   104  		logger.Infof("Unable to get GitHub client: %v", err)
   105  	}
   106  
   107  	for {
   108  		keepMetadataUpdated(netClient, logger)
   109  		if gitHubClient != nil {
   110  			cleanOrphanedPendingMetadata(ctx, gitHubClient, cacheSet, logger, &toBeRemovedPRs)
   111  		} else {
   112  			logger.Infof("GitHub client is not initialized, skipping cleanOrphanedPendingMetadata.")
   113  		}
   114  		time.Sleep(interval)
   115  	}
   116  }
   117  
   118  // keepMetadataUpdated fetches a new copy of the wpt-metadata repo and updates metadataMapCached.
   119  func keepMetadataUpdated(client *http.Client, logger shared.Logger) {
   120  	logger.Infof("Running keepMetadataUpdated...")
   121  	metadataCache, err := shared.GetWPTMetadataArchive(client, nil)
   122  	if err != nil {
   123  		logger.Infof("Error fetching Metadata for update: %v", err)
   124  
   125  		return
   126  	}
   127  
   128  	if metadataCache != nil {
   129  		query.MetadataMapCached = metadataCache
   130  	}
   131  }
   132  
   133  // cleanOrphanedPendingMetadata cleans and removes orphaned pending metadata in Redis.
   134  func cleanOrphanedPendingMetadata(
   135  	ctx context.Context,
   136  	ghClient *github.Client,
   137  	cacheSet shared.RedisSet,
   138  	logger shared.Logger,
   139  	toBeRemovedPRs *[]string,
   140  ) {
   141  	logger.Infof("Running cleanOrphanedPendingMetadata...")
   142  
   143  	for _, pr := range *toBeRemovedPRs {
   144  		logger.Infof("Removing PR %s and its pending metadata from Redis", pr)
   145  		err := cacheSet.Remove(shared.PendingMetadataCacheKey, pr)
   146  		if err != nil {
   147  			logger.Warningf("Error removing %s from RedisSet: %s", pr, err.Error())
   148  		}
   149  		err = shared.DeleteCache(shared.PendingMetadataCachePrefix + pr)
   150  		if err != nil {
   151  			logger.Warningf("Error removing %s from Redis: %s", pr, err.Error())
   152  		}
   153  	}
   154  
   155  	prs, err := cacheSet.GetAll(shared.PendingMetadataCacheKey)
   156  	if err != nil {
   157  		logger.Infof("Error fetching pending PRs from cacheSet: %v", err)
   158  
   159  		return
   160  	}
   161  	logger.Infof("Pending PR numbers in cacheSet are: %v", prs)
   162  
   163  	newRemovePRs := make([]string, 0)
   164  	for _, pr := range prs {
   165  		// Parse PR string into integer
   166  		prInt, err := strconv.Atoi(pr)
   167  		if err != nil {
   168  			logger.Infof("Error parsing %s into integer in cleanOrphanedPendingMetadata", pr)
   169  			// Not an integer; remove it.
   170  			newRemovePRs = append(newRemovePRs, pr)
   171  
   172  			continue
   173  		}
   174  
   175  		res, _, err := ghClient.PullRequests.Get(ctx, shared.SourceOwner, shared.SourceRepo, prInt)
   176  		if err != nil {
   177  			logger.Infof("Error getting information for PR %s: %v", pr, err)
   178  
   179  			continue
   180  		}
   181  
   182  		if res.State == nil || *res.State != "closed" {
   183  			continue
   184  		}
   185  		newRemovePRs = append(newRemovePRs, pr)
   186  	}
   187  	*toBeRemovedPRs = newRemovePRs
   188  }
   189  
   190  // StartWebFeaturesManifestPollingService performs web features manifest related
   191  // services via simple polling every interval duration.
   192  func StartWebFeaturesManifestPollingService(ctx context.Context, logger shared.Logger, interval time.Duration) {
   193  	logger.Infof("Starting web features manifest polling service.")
   194  	gitHubClient, err := shared.NewAppEngineAPI(ctx).GetGitHubClient()
   195  	if err != nil {
   196  		logger.Infof("Unable to get GitHub client: %v", err)
   197  	}
   198  
   199  	for {
   200  		if gitHubClient != nil {
   201  			keepWebFeaturesManifestUpdated(
   202  				ctx,
   203  				logger,
   204  				shared.NewGitHubWebFeaturesClient(gitHubClient))
   205  		} else {
   206  			logger.Infof("GitHub client is not initialized, skipping keepWebFeaturesManifestUpdated.")
   207  		}
   208  		time.Sleep(interval)
   209  	}
   210  }
   211  
   212  // webFeaturesGetter provides a thin interface to get web features data.
   213  type webFeaturesGetter interface {
   214  	Get(context.Context) (shared.WebFeaturesData, error)
   215  }
   216  
   217  // keepWebFeaturesManifestUpdated fetches a new copy of the web features data
   218  // and updates the local cache.
   219  func keepWebFeaturesManifestUpdated(
   220  	ctx context.Context,
   221  	logger shared.Logger,
   222  	featuresGetter webFeaturesGetter) {
   223  	logger.Infof("Running keepWebFeaturesManifestUpdated...")
   224  
   225  	data, err := featuresGetter.Get(ctx)
   226  	if err != nil {
   227  		logger.Errorf("unable to fetch web features manifest during query. %s", err.Error())
   228  
   229  		return
   230  	}
   231  	if data != nil {
   232  		query.SetWebFeaturesDataCache(data)
   233  	}
   234  }