blob: 900085710a5df213d075c4857e889d8289af11dc [file] [log] [blame]
Brad Fitzpatrickb1ddf2b2017-02-08 06:05:26 +00001// 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".
11package maintner
12
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +000013import (
14 "context"
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +000015 "errors"
Brad Fitzpatrick776028b2017-02-15 10:46:56 -080016 "fmt"
Brad Fitzpatrick776028b2017-02-15 10:46:56 -080017 "log"
Brad Fitzpatrick147e8962017-03-08 02:43:47 +000018 "regexp"
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +000019 "sync"
20 "time"
21
Kevin Burke92164ac2017-03-18 17:11:31 -070022 "github.com/golang/protobuf/ptypes"
23 "github.com/golang/protobuf/ptypes/timestamp"
24
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +000025 "golang.org/x/build/maintner/maintpb"
Kevin Burke7ebe3f62017-02-20 13:59:38 -080026 "golang.org/x/sync/errgroup"
Chris Broadfootf7448822018-02-23 11:21:05 -080027 "golang.org/x/time/rate"
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +000028)
Brad Fitzpatrickb1ddf2b2017-02-08 06:05:26 +000029
30// Corpus holds all of a project's metadata.
31type Corpus struct {
Brad Fitzpatrick04f8c522017-04-29 07:39:57 +000032 mutationLogger MutationLogger // non-nil when this is a self-updating corpus
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +000033 mutationSource MutationSource // from Initialize
Brad Fitzpatrick04f8c522017-04-29 07:39:57 +000034 verbose bool
35 dataDir string
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +000036 sawErrSplit bool
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +000037
Brad Fitzpatricka7d233f2017-03-08 16:56:47 -080038 mu sync.RWMutex // guards all following fields
39 // corpus state:
Brad Fitzpatrick1cf306e2017-03-27 17:39:01 +000040 didInit bool // true after Initialize completes successfully
Kevin Burke8cb84082017-03-03 11:56:12 -080041 debug bool
Brad Fitzpatrick059b9082017-03-29 01:10:53 +000042 strIntern map[string]string // interned strings, including binary githashes
43
Brad Fitzpatrickda737d32017-04-05 23:30:39 +000044 // pubsub:
45 activityChans map[string]chan struct{} // keyed by topic
46
Brad Fitzpatricka7d233f2017-03-08 16:56:47 -080047 // github-specific
Brad Fitzpatrickb3a49f92017-03-20 19:58:04 +000048 github *GitHub
Kevin Burke92164ac2017-03-18 17:11:31 -070049 gerrit *Gerrit
Brad Fitzpatrick8e7c0db2017-03-13 20:51:23 +000050 watchedGithubRepos []watchedGithubRepo
Kevin Burke92164ac2017-03-18 17:11:31 -070051 watchedGerritRepos []watchedGerritRepo
Chris Broadfootf7448822018-02-23 11:21:05 -080052 githubLimiter *rate.Limiter
53
Brad Fitzpatricka7d233f2017-03-08 16:56:47 -080054 // git-specific:
Brad Fitzpatrick5bc3f1f2017-03-27 18:44:23 +000055 lastGitCount time.Time // last time of log spam about loading status
Brad Fitzpatricka7d233f2017-03-08 16:56:47 -080056 pollGitDirs []polledGitCommits
Kevin Burkef2033b72017-03-27 13:36:47 -060057 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
Brad Fitzpatricka7d233f2017-03-08 16:56:47 -080061 zoneCache map[string]*time.Location // "+0530" => location
Kevin Burke7ebe3f62017-02-20 13:59:38 -080062}
63
Brad Fitzpatrickb77708e2017-05-16 14:10:39 -070064// RLock grabs the corpus's read lock. Grabbing the read lock prevents
Chris Broadfoot90850ed2017-07-08 09:08:42 -070065// any concurrent writes from mutating the corpus. This is only
Brad Fitzpatrickb77708e2017-05-16 14:10:39 -070066// necessary if the application is querying the corpus and calling its
67// Update method concurrently.
68func (c *Corpus) RLock() { c.mu.RLock() }
69
70// RUnlock unlocks the corpus's read lock.
71func (c *Corpus) RUnlock() { c.mu.RUnlock() }
72
Brad Fitzpatrick147e8962017-03-08 02:43:47 +000073type polledGitCommits struct {
74 repo *maintpb.GitRepo
75 dir string
76}
77
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +000078// EnableLeaderMode prepares c to be the leader. This should only be
79// called by the maintnerd process.
80//
Brad Fitzpatrick04f8c522017-04-29 07:39:57 +000081// The provided scratchDir will store git checkouts.
82func (c *Corpus) EnableLeaderMode(logger MutationLogger, scratchDir string) {
83 c.mutationLogger = logger
84 c.dataDir = scratchDir
85}
86
Brad Fitzpatrickb77708e2017-05-16 14:10:39 -070087// SetVerbose enables or disables verbose logging.
Brad Fitzpatrick04f8c522017-04-29 07:39:57 +000088func (c *Corpus) SetVerbose(v bool) { c.verbose = v }
89
90func (c *Corpus) getDataDir() string {
91 if c.dataDir == "" {
92 panic("getDataDir called before Corpus.EnableLeaderMode")
93 }
94 return c.dataDir
Kevin Burke7ebe3f62017-02-20 13:59:38 -080095}
96
Brad Fitzpatrick3dedafe2017-03-20 23:22:38 +000097// GitHub returns the corpus's github data.
98func (c *Corpus) GitHub() *GitHub {
99 if c.github != nil {
100 return c.github
101 }
102 return new(GitHub)
103}
104
Brad Fitzpatrick1eecef32017-04-03 13:01:31 -0700105// Gerrit returns the corpus's Gerrit data.
106func (c *Corpus) Gerrit() *Gerrit {
107 if c.gerrit != nil {
108 return c.gerrit
109 }
110 return new(Gerrit)
111}
112
Brad Fitzpatrickb77708e2017-05-16 14:10:39 -0700113// Check verifies the internal structure of the Corpus data structures.
114// It is intended for tests and debugging.
115func (c *Corpus) Check() error {
Brad Fitzpatrickae785352017-05-24 16:12:44 +0000116 if err := c.Gerrit().check(); err != nil {
117 return fmt.Errorf("gerrit: %v", err)
Brad Fitzpatrickb77708e2017-05-16 14:10:39 -0700118 }
Brad Fitzpatrick26626972017-11-16 00:30:55 +0000119
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
Brad Fitzpatrickb77708e2017-05-16 14:10:39 -0700134 return nil
135}
136
Kevin Burke92164ac2017-03-18 17:11:31 -0700137// mustProtoFromTime turns a time.Time into a *timestamp.Timestamp or panics if
138// in is invalid.
139func 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
Brad Fitzpatrickd1cc7bf2017-03-09 12:48:04 -0800147// requires c.mu be held for writing
148func (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
Brad Fitzpatrickeadb1bf2017-03-27 18:42:54 +0000159func (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
Kevin Burke8cb84082017-03-03 11:56:12 -0800166func (c *Corpus) SetDebug() {
167 c.debug = true
168}
169
170func (c *Corpus) debugf(format string, v ...interface{}) {
171 if c.debug {
172 log.Printf(format, v...)
173 }
174}
175
Brad Fitzpatrick147e8962017-03-08 02:43:47 +0000176// gerritProjNameRx is the pattern describing a Gerrit project name.
177// TODO: figure out if this is accurate.
178var gerritProjNameRx = regexp.MustCompile(`^[a-z0-9]+[a-z0-9\-\_]*$`)
179
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000180// TrackGoGitRepo registers a git directory to have its metadata slurped into the corpus.
Brad Fitzpatrick147e8962017-03-08 02:43:47 +0000181// The goRepo is a name like "go" or "net". The dir is a path on disk.
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000182func (c *Corpus) TrackGoGitRepo(goRepo, dir string) {
183 if c.mutationLogger == nil {
184 panic("can't TrackGoGitRepo in non-leader mode")
185 }
Brad Fitzpatrick147e8962017-03-08 02:43:47 +0000186 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
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000197// A MutationSource yields a log of mutations that will catch a corpus
198// back up to the present.
199type MutationSource interface {
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000200 // GetMutations returns a channel of mutations or related events.
201 // The channel will never be closed.
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000202 // All sends on the returned channel should select
203 // on the provided context.
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000204 GetMutations(context.Context) <-chan MutationStreamEvent
205}
206
207// MutationStreamEvent represents one of three possible events while
Dmitri Shuralyovbb8466f2020-02-29 00:13:56 -0500208// 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.
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000211type 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
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000223}
224
Brad Fitzpatrick021a49d2017-04-18 18:17:06 +0000225// 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.
Kevin Burke7ebe3f62017-02-20 13:59:38 -0800228func (c *Corpus) Initialize(ctx context.Context, src MutationSource) error {
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000229 if c.mutationSource != nil {
230 panic("duplicate call to Initialize")
231 }
232 c.mutationSource = src
233 log.Printf("Loading data from log %T ...", src)
Andrew Bonventre6990c342017-07-05 22:24:05 -0400234 return c.update(ctx, nil)
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000235}
236
Oleksandr Redkof03e7332023-02-18 15:01:28 +0200237// ErrSplit is returned when the client notices the leader's
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000238// mutation log has changed. This can happen if the leader restarts
239// with uncommitted transactions. (The leader only commits mutations
240// periodically.)
241var 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.
Dmitri Shuralyov816bbcc2022-06-25 13:12:59 -0400247// If Update returns ErrSplit, the corpus can no longer be updated.
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000248//
Chris Broadfoot90850ed2017-07-08 09:08:42 -0700249// 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.
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000252func (c *Corpus) Update(ctx context.Context) error {
253 if c.mutationSource == nil {
Andrew Bonventre6990c342017-07-05 22:24:05 -0400254 panic("Update called without call to Initialize")
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000255 }
256 if c.sawErrSplit {
Andrew Bonventre6990c342017-07-05 22:24:05 -0400257 panic("Update called after previous call returned ErrSplit")
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000258 }
259 log.Printf("Updating data from log %T ...", c.mutationSource)
Andrew Bonventre6990c342017-07-05 22:24:05 -0400260 err := c.update(ctx, nil)
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000261 if err == ErrSplit {
262 c.sawErrSplit = true
263 }
264 return err
265}
266
Andrew Bonventre6990c342017-07-05 22:24:05 -0400267// UpdateWithLocker behaves just like Update, but holds lk when processing
268// mutation events.
269func (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
Brad Fitzpatrickee6321b2017-11-14 23:25:00 +0000284type noopLocker struct{}
285
286func (noopLocker) Lock() {}
287func (noopLocker) Unlock() {}
288
Andrew Bonventre6990c342017-07-05 22:24:05 -0400289// lk optionally specifies a locker to use while processing mutations.
290func (c *Corpus) update(ctx context.Context, lk sync.Locker) error {
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000291 src := c.mutationSource
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000292 ch := src.GetMutations(ctx)
293 done := ctx.Done()
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000294 c.mu.Lock()
295 defer c.mu.Unlock()
Brad Fitzpatrickee6321b2017-11-14 23:25:00 +0000296 if lk == nil {
297 lk = noopLocker{}
298 }
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000299 for {
300 select {
301 case <-done:
Brad Fitzpatricka7d233f2017-03-08 16:56:47 -0800302 err := ctx.Err()
303 log.Printf("Context expired while loading data from log %T: %v", src, err)
304 return err
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000305 case e := <-ch:
306 if e.Err != nil {
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000307 log.Printf("Corpus GetMutations: %v", e.Err)
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000308 return e.Err
309 }
310 if e.End {
Brad Fitzpatrick1cf306e2017-03-27 17:39:01 +0000311 c.didInit = true
Brad Fitzpatrickee6321b2017-11-14 23:25:00 +0000312 lk.Lock()
313 c.finishProcessing()
314 lk.Unlock()
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000315 log.Printf("Reloaded data from log %T.", src)
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000316 return nil
317 }
Brad Fitzpatrickee6321b2017-11-14 23:25:00 +0000318 lk.Lock()
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000319 c.processMutationLocked(e.Mutation)
Brad Fitzpatrickee6321b2017-11-14 23:25:00 +0000320 lk.Unlock()
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000321 }
322 }
323}
324
Brad Fitzpatrick2ceb7572017-03-25 03:56:50 +0000325// addMutation adds a mutation to the log and immediately processes it.
326func (c *Corpus) addMutation(m *maintpb.Mutation) {
Brad Fitzpatrick04f8c522017-04-29 07:39:57 +0000327 if c.verbose {
Brad Fitzpatrick8927fdd2017-03-15 17:51:41 +0000328 log.Printf("mutation: %v", m)
329 }
Kevin Burke9fd5f302017-02-28 13:31:57 -0800330 c.mu.Lock()
331 c.processMutationLocked(m)
Brad Fitzpatrick02c3a362017-11-17 21:48:51 +0000332 c.finishProcessing()
Kevin Burke9fd5f302017-02-28 13:31:57 -0800333 c.mu.Unlock()
Brad Fitzpatrick2ceb7572017-03-25 03:56:50 +0000334
Brad Fitzpatrick04f8c522017-04-29 07:39:57 +0000335 if c.mutationLogger == nil {
Brad Fitzpatrick2ceb7572017-03-25 03:56:50 +0000336 return
337 }
Brad Fitzpatrick04f8c522017-04-29 07:39:57 +0000338 err := c.mutationLogger.Log(m)
Brad Fitzpatrick2ceb7572017-03-25 03:56:50 +0000339 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)
Kevin Burke9fd5f302017-02-28 13:31:57 -0800342 }
343}
344
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000345// c.mu must be held.
346func (c *Corpus) processMutationLocked(m *maintpb.Mutation) {
347 if im := m.GithubIssue; im != nil {
348 c.processGithubIssueMutation(im)
349 }
Brad Fitzpatrick8cf2b3b2017-03-18 03:48:31 +0000350 if gm := m.Github; gm != nil {
351 c.processGithubMutation(gm)
352 }
Brad Fitzpatricka7d233f2017-03-08 16:56:47 -0800353 if gm := m.Git; gm != nil {
354 c.processGitMutation(gm)
355 }
Kevin Burke92164ac2017-03-18 17:11:31 -0700356 if gm := m.Gerrit; gm != nil {
357 c.processGerritMutation(gm)
358 }
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000359}
360
Brad Fitzpatrickee6321b2017-11-14 23:25:00 +0000361// 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.
365func (c *Corpus) finishProcessing() {
366 c.gerrit.finishProcessing()
367}
368
Brad Fitzpatrick8da4ff02017-03-20 22:18:18 +0000369// SyncLoop runs forever (until an error or context expiration) and
370// updates the corpus as the tracked sources change.
371func (c *Corpus) SyncLoop(ctx context.Context) error {
372 return c.sync(ctx, true)
Brad Fitzpatrickb1ddf2b2017-02-08 06:05:26 +0000373}
374
Brad Fitzpatrick8da4ff02017-03-20 22:18:18 +0000375// Sync updates the corpus from its tracked sources.
376func (c *Corpus) Sync(ctx context.Context) error {
377 return c.sync(ctx, false)
Brad Fitzpatrickb1ddf2b2017-02-08 06:05:26 +0000378}
379
Brad Fitzpatrick8da4ff02017-03-20 22:18:18 +0000380func (c *Corpus) sync(ctx context.Context, loop bool) error {
Brad Fitzpatrick73e10fb2017-05-16 17:36:42 -0700381 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
Kevin Burke7ebe3f62017-02-20 13:59:38 -0800385 group, ctx := errgroup.WithContext(ctx)
Brad Fitzpatrick8927fdd2017-03-15 17:51:41 +0000386 for _, w := range c.watchedGithubRepos {
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000387 gr, token := w.gr, w.token
Kevin Burke7ebe3f62017-02-20 13:59:38 -0800388 group.Go(func() error {
Brad Fitzpatrick8927fdd2017-03-15 17:51:41 +0000389 log.Printf("Polling %v ...", gr.id)
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000390 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 }
Kevin Burke7ebe3f62017-02-20 13:59:38 -0800400 })
401 }
Brad Fitzpatrick147e8962017-03-08 02:43:47 +0000402 for _, rp := range c.pollGitDirs {
403 rp := rp
404 group.Go(func() error {
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000405 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 }
Brad Fitzpatrick147e8962017-03-08 02:43:47 +0000415 })
416 }
Kevin Burke92164ac2017-03-18 17:11:31 -0700417 for _, w := range c.watchedGerritRepos {
Brad Fitzpatrickd97cc622017-03-24 23:23:46 +0000418 gp := w.project
Kevin Burke92164ac2017-03-18 17:11:31 -0700419 group.Go(func() error {
Brad Fitzpatrickd97cc622017-03-24 23:23:46 +0000420 log.Printf("Polling gerrit %v ...", gp.proj)
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000421 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 }
Kevin Burke92164ac2017-03-18 17:11:31 -0700431 })
432 }
Kevin Burke7ebe3f62017-02-20 13:59:38 -0800433 return group.Wait()
434}
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000435
436func isTempErr(err error) bool {
437 log.Printf("IS TEMP ERROR? %T %v", err, err)
438 return true
439}