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  }