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 }