golang.org/x/build@v0.0.0-20240506185731-218518f32b70/maintner/maintner.go (about) 1 // Copyright 2017 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package maintner mirrors, searches, syncs, and serves Git, Github, 6 // and Gerrit metadata. 7 // 8 // Maintner is short for "Maintainer". This package is intended for 9 // use by many tools. The name of the daemon that serves the maintner 10 // data to other tools is "maintnerd". 11 package maintner 12 13 import ( 14 "context" 15 "errors" 16 "fmt" 17 "log" 18 "regexp" 19 "sync" 20 "time" 21 22 "github.com/golang/protobuf/ptypes" 23 "github.com/golang/protobuf/ptypes/timestamp" 24 25 "golang.org/x/build/maintner/maintpb" 26 "golang.org/x/sync/errgroup" 27 "golang.org/x/time/rate" 28 ) 29 30 // Corpus holds all of a project's metadata. 31 type Corpus struct { 32 mutationLogger MutationLogger // non-nil when this is a self-updating corpus 33 mutationSource MutationSource // from Initialize 34 verbose bool 35 dataDir string 36 sawErrSplit bool 37 38 mu sync.RWMutex // guards all following fields 39 // corpus state: 40 didInit bool // true after Initialize completes successfully 41 debug bool 42 strIntern map[string]string // interned strings, including binary githashes 43 44 // pubsub: 45 activityChans map[string]chan struct{} // keyed by topic 46 47 // github-specific 48 github *GitHub 49 gerrit *Gerrit 50 watchedGithubRepos []watchedGithubRepo 51 watchedGerritRepos []watchedGerritRepo 52 githubLimiter *rate.Limiter 53 54 // git-specific: 55 lastGitCount time.Time // last time of log spam about loading status 56 pollGitDirs []polledGitCommits 57 gitPeople map[string]*GitPerson 58 gitCommit map[GitHash]*GitCommit 59 gitCommitTodo map[GitHash]bool // -> true 60 gitOfHg map[string]GitHash // hg hex hash -> git hash 61 zoneCache map[string]*time.Location // "+0530" => location 62 } 63 64 // RLock grabs the corpus's read lock. Grabbing the read lock prevents 65 // any concurrent writes from mutating the corpus. This is only 66 // necessary if the application is querying the corpus and calling its 67 // Update method concurrently. 68 func (c *Corpus) RLock() { c.mu.RLock() } 69 70 // RUnlock unlocks the corpus's read lock. 71 func (c *Corpus) RUnlock() { c.mu.RUnlock() } 72 73 type polledGitCommits struct { 74 repo *maintpb.GitRepo 75 dir string 76 } 77 78 // EnableLeaderMode prepares c to be the leader. This should only be 79 // called by the maintnerd process. 80 // 81 // The provided scratchDir will store git checkouts. 82 func (c *Corpus) EnableLeaderMode(logger MutationLogger, scratchDir string) { 83 c.mutationLogger = logger 84 c.dataDir = scratchDir 85 } 86 87 // SetVerbose enables or disables verbose logging. 88 func (c *Corpus) SetVerbose(v bool) { c.verbose = v } 89 90 func (c *Corpus) getDataDir() string { 91 if c.dataDir == "" { 92 panic("getDataDir called before Corpus.EnableLeaderMode") 93 } 94 return c.dataDir 95 } 96 97 // GitHub returns the corpus's github data. 98 func (c *Corpus) GitHub() *GitHub { 99 if c.github != nil { 100 return c.github 101 } 102 return new(GitHub) 103 } 104 105 // Gerrit returns the corpus's Gerrit data. 106 func (c *Corpus) Gerrit() *Gerrit { 107 if c.gerrit != nil { 108 return c.gerrit 109 } 110 return new(Gerrit) 111 } 112 113 // Check verifies the internal structure of the Corpus data structures. 114 // It is intended for tests and debugging. 115 func (c *Corpus) Check() error { 116 if err := c.Gerrit().check(); err != nil { 117 return fmt.Errorf("gerrit: %v", err) 118 } 119 120 for hash, gc := range c.gitCommit { 121 if gc.Committer == placeholderCommitter { 122 return fmt.Errorf("corpus git commit %v has placeholder committer", hash) 123 } 124 if gc.Hash != hash { 125 return fmt.Errorf("git commit for key %q had GitCommit.Hash %q", hash, gc.Hash) 126 } 127 for _, pc := range gc.Parents { 128 if _, ok := c.gitCommit[pc.Hash]; !ok { 129 return fmt.Errorf("git commit %q exists but its parent %q does not", gc.Hash, pc.Hash) 130 } 131 } 132 } 133 134 return nil 135 } 136 137 // mustProtoFromTime turns a time.Time into a *timestamp.Timestamp or panics if 138 // in is invalid. 139 func mustProtoFromTime(in time.Time) *timestamp.Timestamp { 140 tp, err := ptypes.TimestampProto(in) 141 if err != nil { 142 panic(err) 143 } 144 return tp 145 } 146 147 // requires c.mu be held for writing 148 func (c *Corpus) str(s string) string { 149 if v, ok := c.strIntern[s]; ok { 150 return v 151 } 152 if c.strIntern == nil { 153 c.strIntern = make(map[string]string) 154 } 155 c.strIntern[s] = s 156 return s 157 } 158 159 func (c *Corpus) strb(b []byte) string { 160 if v, ok := c.strIntern[string(b)]; ok { 161 return v 162 } 163 return c.str(string(b)) 164 } 165 166 func (c *Corpus) SetDebug() { 167 c.debug = true 168 } 169 170 func (c *Corpus) debugf(format string, v ...interface{}) { 171 if c.debug { 172 log.Printf(format, v...) 173 } 174 } 175 176 // gerritProjNameRx is the pattern describing a Gerrit project name. 177 // TODO: figure out if this is accurate. 178 var gerritProjNameRx = regexp.MustCompile(`^[a-z0-9]+[a-z0-9\-\_]*$`) 179 180 // TrackGoGitRepo registers a git directory to have its metadata slurped into the corpus. 181 // The goRepo is a name like "go" or "net". The dir is a path on disk. 182 func (c *Corpus) TrackGoGitRepo(goRepo, dir string) { 183 if c.mutationLogger == nil { 184 panic("can't TrackGoGitRepo in non-leader mode") 185 } 186 if !gerritProjNameRx.MatchString(goRepo) { 187 panic(fmt.Sprintf("bogus goRepo value %q", goRepo)) 188 } 189 c.mu.Lock() 190 defer c.mu.Unlock() 191 c.pollGitDirs = append(c.pollGitDirs, polledGitCommits{ 192 repo: &maintpb.GitRepo{GoRepo: goRepo}, 193 dir: dir, 194 }) 195 } 196 197 // A MutationSource yields a log of mutations that will catch a corpus 198 // back up to the present. 199 type MutationSource interface { 200 // GetMutations returns a channel of mutations or related events. 201 // The channel will never be closed. 202 // All sends on the returned channel should select 203 // on the provided context. 204 GetMutations(context.Context) <-chan MutationStreamEvent 205 } 206 207 // MutationStreamEvent represents one of three possible events while 208 // reading mutations from disk or another source. 209 // An event is either a mutation, an error, or reaching the current 210 // end of the log. Exactly one of the three fields will be non-zero. 211 type MutationStreamEvent struct { 212 Mutation *maintpb.Mutation 213 214 // Err is a fatal error reading the log. No other events will 215 // follow an Err. 216 Err error 217 218 // End, if true, means that all mutations have been sent and 219 // the next event might take some time to arrive (it might not 220 // have occurred yet). The End event is not a terminal state 221 // like Err. There may be multiple Ends. 222 End bool 223 } 224 225 // Initialize populates the Corpus using the data from the 226 // MutationSource. It returns once it's up-to-date. To incrementally 227 // update it later, use the Update method. 228 func (c *Corpus) Initialize(ctx context.Context, src MutationSource) error { 229 if c.mutationSource != nil { 230 panic("duplicate call to Initialize") 231 } 232 c.mutationSource = src 233 log.Printf("Loading data from log %T ...", src) 234 return c.update(ctx, nil) 235 } 236 237 // ErrSplit is returned when the client notices the leader's 238 // mutation log has changed. This can happen if the leader restarts 239 // with uncommitted transactions. (The leader only commits mutations 240 // periodically.) 241 var ErrSplit = errors.New("maintner: leader server's history split, process out of sync") 242 243 // Update incrementally updates the corpus from its current state to 244 // the latest state from the MutationSource passed earlier to 245 // Initialize. It does not return until there's either a new change or 246 // the context expires. 247 // If Update returns ErrSplit, the corpus can no longer be updated. 248 // 249 // Update must not be called concurrently with any other Update calls. If 250 // reading the corpus concurrently while the corpus is updating, you must hold 251 // the read lock using Corpus.RLock. 252 func (c *Corpus) Update(ctx context.Context) error { 253 if c.mutationSource == nil { 254 panic("Update called without call to Initialize") 255 } 256 if c.sawErrSplit { 257 panic("Update called after previous call returned ErrSplit") 258 } 259 log.Printf("Updating data from log %T ...", c.mutationSource) 260 err := c.update(ctx, nil) 261 if err == ErrSplit { 262 c.sawErrSplit = true 263 } 264 return err 265 } 266 267 // UpdateWithLocker behaves just like Update, but holds lk when processing 268 // mutation events. 269 func (c *Corpus) UpdateWithLocker(ctx context.Context, lk sync.Locker) error { 270 if c.mutationSource == nil { 271 panic("UpdateWithLocker called without call to Initialize") 272 } 273 if c.sawErrSplit { 274 panic("UpdateWithLocker called after previous call returned ErrSplit") 275 } 276 log.Printf("Updating data from log %T ...", c.mutationSource) 277 err := c.update(ctx, lk) 278 if err == ErrSplit { 279 c.sawErrSplit = true 280 } 281 return err 282 } 283 284 type noopLocker struct{} 285 286 func (noopLocker) Lock() {} 287 func (noopLocker) Unlock() {} 288 289 // lk optionally specifies a locker to use while processing mutations. 290 func (c *Corpus) update(ctx context.Context, lk sync.Locker) error { 291 src := c.mutationSource 292 ch := src.GetMutations(ctx) 293 done := ctx.Done() 294 c.mu.Lock() 295 defer c.mu.Unlock() 296 if lk == nil { 297 lk = noopLocker{} 298 } 299 for { 300 select { 301 case <-done: 302 err := ctx.Err() 303 log.Printf("Context expired while loading data from log %T: %v", src, err) 304 return err 305 case e := <-ch: 306 if e.Err != nil { 307 log.Printf("Corpus GetMutations: %v", e.Err) 308 return e.Err 309 } 310 if e.End { 311 c.didInit = true 312 lk.Lock() 313 c.finishProcessing() 314 lk.Unlock() 315 log.Printf("Reloaded data from log %T.", src) 316 return nil 317 } 318 lk.Lock() 319 c.processMutationLocked(e.Mutation) 320 lk.Unlock() 321 } 322 } 323 } 324 325 // addMutation adds a mutation to the log and immediately processes it. 326 func (c *Corpus) addMutation(m *maintpb.Mutation) { 327 if c.verbose { 328 log.Printf("mutation: %v", m) 329 } 330 c.mu.Lock() 331 c.processMutationLocked(m) 332 c.finishProcessing() 333 c.mu.Unlock() 334 335 if c.mutationLogger == nil { 336 return 337 } 338 err := c.mutationLogger.Log(m) 339 if err != nil { 340 // TODO: handle errors better? failing is only safe option. 341 log.Fatalf("could not log mutation %v: %v\n", m, err) 342 } 343 } 344 345 // c.mu must be held. 346 func (c *Corpus) processMutationLocked(m *maintpb.Mutation) { 347 if im := m.GithubIssue; im != nil { 348 c.processGithubIssueMutation(im) 349 } 350 if gm := m.Github; gm != nil { 351 c.processGithubMutation(gm) 352 } 353 if gm := m.Git; gm != nil { 354 c.processGitMutation(gm) 355 } 356 if gm := m.Gerrit; gm != nil { 357 c.processGerritMutation(gm) 358 } 359 } 360 361 // finishProcessing fixes up invariants and data structures before 362 // returning the Corpus from the Update loop back to the user. 363 // 364 // c.mu must be held. 365 func (c *Corpus) finishProcessing() { 366 c.gerrit.finishProcessing() 367 } 368 369 // SyncLoop runs forever (until an error or context expiration) and 370 // updates the corpus as the tracked sources change. 371 func (c *Corpus) SyncLoop(ctx context.Context) error { 372 return c.sync(ctx, true) 373 } 374 375 // Sync updates the corpus from its tracked sources. 376 func (c *Corpus) Sync(ctx context.Context) error { 377 return c.sync(ctx, false) 378 } 379 380 func (c *Corpus) sync(ctx context.Context, loop bool) error { 381 if _, ok := c.mutationSource.(*netMutSource); ok { 382 return errors.New("maintner: can't run Corpus.Sync on a Corpus using NetworkMutationSource (did you mean Update?)") 383 } 384 385 group, ctx := errgroup.WithContext(ctx) 386 for _, w := range c.watchedGithubRepos { 387 gr, token := w.gr, w.token 388 group.Go(func() error { 389 log.Printf("Polling %v ...", gr.id) 390 for { 391 err := gr.sync(ctx, token, loop) 392 if loop && isTempErr(err) { 393 log.Printf("Temporary error from github %v: %v", gr.ID(), err) 394 time.Sleep(30 * time.Second) 395 continue 396 } 397 log.Printf("github sync ending for %v: %v", gr.ID(), err) 398 return err 399 } 400 }) 401 } 402 for _, rp := range c.pollGitDirs { 403 rp := rp 404 group.Go(func() error { 405 for { 406 err := c.syncGitCommits(ctx, rp, loop) 407 if loop && isTempErr(err) { 408 log.Printf("Temporary error from git repo %v: %v", rp.dir, err) 409 time.Sleep(30 * time.Second) 410 continue 411 } 412 log.Printf("git sync ending for %v: %v", rp.dir, err) 413 return err 414 } 415 }) 416 } 417 for _, w := range c.watchedGerritRepos { 418 gp := w.project 419 group.Go(func() error { 420 log.Printf("Polling gerrit %v ...", gp.proj) 421 for { 422 err := gp.sync(ctx, loop) 423 if loop && isTempErr(err) { 424 log.Printf("Temporary error from gerrit %v: %v", gp.proj, err) 425 time.Sleep(30 * time.Second) 426 continue 427 } 428 log.Printf("gerrit sync ending for %v: %v", gp.proj, err) 429 return err 430 } 431 }) 432 } 433 return group.Wait() 434 } 435 436 func isTempErr(err error) bool { 437 log.Printf("IS TEMP ERROR? %T %v", err, err) 438 return true 439 }