blob: 26a8bf63dd5836a66785b0230e2c55a1cf833d48 [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.
Kevin Burke7ebe3f62017-02-20 13:59:38 -080031//
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +000032// Many public accessor methods are missing. File bugs at golang.org/issues/new.
Brad Fitzpatrickb1ddf2b2017-02-08 06:05:26 +000033type Corpus struct {
Brad Fitzpatrick04f8c522017-04-29 07:39:57 +000034 mutationLogger MutationLogger // non-nil when this is a self-updating corpus
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +000035 mutationSource MutationSource // from Initialize
Brad Fitzpatrick04f8c522017-04-29 07:39:57 +000036 verbose bool
37 dataDir string
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +000038 sawErrSplit bool
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +000039
Brad Fitzpatricka7d233f2017-03-08 16:56:47 -080040 mu sync.RWMutex // guards all following fields
41 // corpus state:
Brad Fitzpatrick1cf306e2017-03-27 17:39:01 +000042 didInit bool // true after Initialize completes successfully
Kevin Burke8cb84082017-03-03 11:56:12 -080043 debug bool
Brad Fitzpatrick059b9082017-03-29 01:10:53 +000044 strIntern map[string]string // interned strings, including binary githashes
45
Brad Fitzpatrickda737d32017-04-05 23:30:39 +000046 // pubsub:
47 activityChans map[string]chan struct{} // keyed by topic
48
Brad Fitzpatricka7d233f2017-03-08 16:56:47 -080049 // github-specific
Brad Fitzpatrickb3a49f92017-03-20 19:58:04 +000050 github *GitHub
Kevin Burke92164ac2017-03-18 17:11:31 -070051 gerrit *Gerrit
Brad Fitzpatrick8e7c0db2017-03-13 20:51:23 +000052 watchedGithubRepos []watchedGithubRepo
Kevin Burke92164ac2017-03-18 17:11:31 -070053 watchedGerritRepos []watchedGerritRepo
Chris Broadfootf7448822018-02-23 11:21:05 -080054 githubLimiter *rate.Limiter
55
Brad Fitzpatricka7d233f2017-03-08 16:56:47 -080056 // git-specific:
Brad Fitzpatrick5bc3f1f2017-03-27 18:44:23 +000057 lastGitCount time.Time // last time of log spam about loading status
Brad Fitzpatricka7d233f2017-03-08 16:56:47 -080058 pollGitDirs []polledGitCommits
Kevin Burkef2033b72017-03-27 13:36:47 -060059 gitPeople map[string]*GitPerson
60 gitCommit map[GitHash]*GitCommit
61 gitCommitTodo map[GitHash]bool // -> true
62 gitOfHg map[string]GitHash // hg hex hash -> git hash
Brad Fitzpatricka7d233f2017-03-08 16:56:47 -080063 zoneCache map[string]*time.Location // "+0530" => location
Kevin Burke7ebe3f62017-02-20 13:59:38 -080064}
65
Brad Fitzpatrickb77708e2017-05-16 14:10:39 -070066// RLock grabs the corpus's read lock. Grabbing the read lock prevents
Chris Broadfoot90850ed2017-07-08 09:08:42 -070067// any concurrent writes from mutating the corpus. This is only
Brad Fitzpatrickb77708e2017-05-16 14:10:39 -070068// necessary if the application is querying the corpus and calling its
69// Update method concurrently.
70func (c *Corpus) RLock() { c.mu.RLock() }
71
72// RUnlock unlocks the corpus's read lock.
73func (c *Corpus) RUnlock() { c.mu.RUnlock() }
74
Brad Fitzpatrick147e8962017-03-08 02:43:47 +000075type polledGitCommits struct {
76 repo *maintpb.GitRepo
77 dir string
78}
79
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +000080// EnableLeaderMode prepares c to be the leader. This should only be
81// called by the maintnerd process.
82//
Brad Fitzpatrick04f8c522017-04-29 07:39:57 +000083// The provided scratchDir will store git checkouts.
84func (c *Corpus) EnableLeaderMode(logger MutationLogger, scratchDir string) {
85 c.mutationLogger = logger
86 c.dataDir = scratchDir
87}
88
Brad Fitzpatrickb77708e2017-05-16 14:10:39 -070089// SetVerbose enables or disables verbose logging.
Brad Fitzpatrick04f8c522017-04-29 07:39:57 +000090func (c *Corpus) SetVerbose(v bool) { c.verbose = v }
91
92func (c *Corpus) getDataDir() string {
93 if c.dataDir == "" {
94 panic("getDataDir called before Corpus.EnableLeaderMode")
95 }
96 return c.dataDir
Kevin Burke7ebe3f62017-02-20 13:59:38 -080097}
98
Brad Fitzpatrick3dedafe2017-03-20 23:22:38 +000099// GitHub returns the corpus's github data.
100func (c *Corpus) GitHub() *GitHub {
101 if c.github != nil {
102 return c.github
103 }
104 return new(GitHub)
105}
106
Brad Fitzpatrick1eecef32017-04-03 13:01:31 -0700107// Gerrit returns the corpus's Gerrit data.
108func (c *Corpus) Gerrit() *Gerrit {
109 if c.gerrit != nil {
110 return c.gerrit
111 }
112 return new(Gerrit)
113}
114
Brad Fitzpatrickb77708e2017-05-16 14:10:39 -0700115// Check verifies the internal structure of the Corpus data structures.
116// It is intended for tests and debugging.
117func (c *Corpus) Check() error {
Brad Fitzpatrickae785352017-05-24 16:12:44 +0000118 if err := c.Gerrit().check(); err != nil {
119 return fmt.Errorf("gerrit: %v", err)
Brad Fitzpatrickb77708e2017-05-16 14:10:39 -0700120 }
Brad Fitzpatrick26626972017-11-16 00:30:55 +0000121
122 for hash, gc := range c.gitCommit {
123 if gc.Committer == placeholderCommitter {
124 return fmt.Errorf("corpus git commit %v has placeholder committer", hash)
125 }
126 if gc.Hash != hash {
127 return fmt.Errorf("git commit for key %q had GitCommit.Hash %q", hash, gc.Hash)
128 }
129 for _, pc := range gc.Parents {
130 if _, ok := c.gitCommit[pc.Hash]; !ok {
131 return fmt.Errorf("git commit %q exists but its parent %q does not", gc.Hash, pc.Hash)
132 }
133 }
134 }
135
Brad Fitzpatrickb77708e2017-05-16 14:10:39 -0700136 return nil
137}
138
Kevin Burke92164ac2017-03-18 17:11:31 -0700139// mustProtoFromTime turns a time.Time into a *timestamp.Timestamp or panics if
140// in is invalid.
141func mustProtoFromTime(in time.Time) *timestamp.Timestamp {
142 tp, err := ptypes.TimestampProto(in)
143 if err != nil {
144 panic(err)
145 }
146 return tp
147}
148
Brad Fitzpatrickd1cc7bf2017-03-09 12:48:04 -0800149// requires c.mu be held for writing
150func (c *Corpus) str(s string) string {
151 if v, ok := c.strIntern[s]; ok {
152 return v
153 }
154 if c.strIntern == nil {
155 c.strIntern = make(map[string]string)
156 }
157 c.strIntern[s] = s
158 return s
159}
160
Brad Fitzpatrickeadb1bf2017-03-27 18:42:54 +0000161func (c *Corpus) strb(b []byte) string {
162 if v, ok := c.strIntern[string(b)]; ok {
163 return v
164 }
165 return c.str(string(b))
166}
167
Kevin Burke8cb84082017-03-03 11:56:12 -0800168func (c *Corpus) SetDebug() {
169 c.debug = true
170}
171
172func (c *Corpus) debugf(format string, v ...interface{}) {
173 if c.debug {
174 log.Printf(format, v...)
175 }
176}
177
Brad Fitzpatrick147e8962017-03-08 02:43:47 +0000178// gerritProjNameRx is the pattern describing a Gerrit project name.
179// TODO: figure out if this is accurate.
180var gerritProjNameRx = regexp.MustCompile(`^[a-z0-9]+[a-z0-9\-\_]*$`)
181
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000182// TrackGoGitRepo registers a git directory to have its metadata slurped into the corpus.
Brad Fitzpatrick147e8962017-03-08 02:43:47 +0000183// The goRepo is a name like "go" or "net". The dir is a path on disk.
184//
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000185func (c *Corpus) TrackGoGitRepo(goRepo, dir string) {
186 if c.mutationLogger == nil {
187 panic("can't TrackGoGitRepo in non-leader mode")
188 }
Brad Fitzpatrick147e8962017-03-08 02:43:47 +0000189 if !gerritProjNameRx.MatchString(goRepo) {
190 panic(fmt.Sprintf("bogus goRepo value %q", goRepo))
191 }
192 c.mu.Lock()
193 defer c.mu.Unlock()
194 c.pollGitDirs = append(c.pollGitDirs, polledGitCommits{
195 repo: &maintpb.GitRepo{GoRepo: goRepo},
196 dir: dir,
197 })
198}
199
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000200// A MutationSource yields a log of mutations that will catch a corpus
201// back up to the present.
202type MutationSource interface {
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000203 // GetMutations returns a channel of mutations or related events.
204 // The channel will never be closed.
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000205 // All sends on the returned channel should select
206 // on the provided context.
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000207 GetMutations(context.Context) <-chan MutationStreamEvent
208}
209
210// MutationStreamEvent represents one of three possible events while
Dmitri Shuralyovbb8466f2020-02-29 00:13:56 -0500211// reading mutations from disk or another source.
212// An event is either a mutation, an error, or reaching the current
213// end of the log. Exactly one of the three fields will be non-zero.
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000214type MutationStreamEvent struct {
215 Mutation *maintpb.Mutation
216
217 // Err is a fatal error reading the log. No other events will
218 // follow an Err.
219 Err error
220
221 // End, if true, means that all mutations have been sent and
222 // the next event might take some time to arrive (it might not
223 // have occurred yet). The End event is not a terminal state
224 // like Err. There may be multiple Ends.
225 End bool
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000226}
227
Brad Fitzpatrick021a49d2017-04-18 18:17:06 +0000228// Initialize populates the Corpus using the data from the
229// MutationSource. It returns once it's up-to-date. To incrementally
230// update it later, use the Update method.
Kevin Burke7ebe3f62017-02-20 13:59:38 -0800231func (c *Corpus) Initialize(ctx context.Context, src MutationSource) error {
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000232 if c.mutationSource != nil {
233 panic("duplicate call to Initialize")
234 }
235 c.mutationSource = src
236 log.Printf("Loading data from log %T ...", src)
Andrew Bonventre6990c342017-07-05 22:24:05 -0400237 return c.update(ctx, nil)
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000238}
239
240// ErrSplit is returned when the the client notices the leader's
241// mutation log has changed. This can happen if the leader restarts
242// with uncommitted transactions. (The leader only commits mutations
243// periodically.)
244var ErrSplit = errors.New("maintner: leader server's history split, process out of sync")
245
246// Update incrementally updates the corpus from its current state to
247// the latest state from the MutationSource passed earlier to
248// Initialize. It does not return until there's either a new change or
249// the context expires.
250// If Update returns ErrSplit, the corpus can longer be updated.
251//
Chris Broadfoot90850ed2017-07-08 09:08:42 -0700252// Update must not be called concurrently with any other Update calls. If
253// reading the corpus concurrently while the corpus is updating, you must hold
254// the read lock using Corpus.RLock.
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000255func (c *Corpus) Update(ctx context.Context) error {
256 if c.mutationSource == nil {
Andrew Bonventre6990c342017-07-05 22:24:05 -0400257 panic("Update called without call to Initialize")
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000258 }
259 if c.sawErrSplit {
Andrew Bonventre6990c342017-07-05 22:24:05 -0400260 panic("Update called after previous call returned ErrSplit")
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000261 }
262 log.Printf("Updating data from log %T ...", c.mutationSource)
Andrew Bonventre6990c342017-07-05 22:24:05 -0400263 err := c.update(ctx, nil)
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000264 if err == ErrSplit {
265 c.sawErrSplit = true
266 }
267 return err
268}
269
Andrew Bonventre6990c342017-07-05 22:24:05 -0400270// UpdateWithLocker behaves just like Update, but holds lk when processing
271// mutation events.
272func (c *Corpus) UpdateWithLocker(ctx context.Context, lk sync.Locker) error {
273 if c.mutationSource == nil {
274 panic("UpdateWithLocker called without call to Initialize")
275 }
276 if c.sawErrSplit {
277 panic("UpdateWithLocker called after previous call returned ErrSplit")
278 }
279 log.Printf("Updating data from log %T ...", c.mutationSource)
280 err := c.update(ctx, lk)
281 if err == ErrSplit {
282 c.sawErrSplit = true
283 }
284 return err
285}
286
Brad Fitzpatrickee6321b2017-11-14 23:25:00 +0000287type noopLocker struct{}
288
289func (noopLocker) Lock() {}
290func (noopLocker) Unlock() {}
291
Andrew Bonventre6990c342017-07-05 22:24:05 -0400292// lk optionally specifies a locker to use while processing mutations.
293func (c *Corpus) update(ctx context.Context, lk sync.Locker) error {
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000294 src := c.mutationSource
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000295 ch := src.GetMutations(ctx)
296 done := ctx.Done()
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000297 c.mu.Lock()
298 defer c.mu.Unlock()
Brad Fitzpatrickee6321b2017-11-14 23:25:00 +0000299 if lk == nil {
300 lk = noopLocker{}
301 }
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000302 for {
303 select {
304 case <-done:
Brad Fitzpatricka7d233f2017-03-08 16:56:47 -0800305 err := ctx.Err()
306 log.Printf("Context expired while loading data from log %T: %v", src, err)
307 return err
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000308 case e := <-ch:
309 if e.Err != nil {
Brad Fitzpatrick1a1ef8e2017-04-29 21:15:37 +0000310 log.Printf("Corpus GetMutations: %v", e.Err)
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000311 return e.Err
312 }
313 if e.End {
Brad Fitzpatrick1cf306e2017-03-27 17:39:01 +0000314 c.didInit = true
Brad Fitzpatrickee6321b2017-11-14 23:25:00 +0000315 lk.Lock()
316 c.finishProcessing()
317 lk.Unlock()
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000318 log.Printf("Reloaded data from log %T.", src)
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000319 return nil
320 }
Brad Fitzpatrickee6321b2017-11-14 23:25:00 +0000321 lk.Lock()
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000322 c.processMutationLocked(e.Mutation)
Brad Fitzpatrickee6321b2017-11-14 23:25:00 +0000323 lk.Unlock()
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000324 }
325 }
326}
327
Brad Fitzpatrick2ceb7572017-03-25 03:56:50 +0000328// addMutation adds a mutation to the log and immediately processes it.
329func (c *Corpus) addMutation(m *maintpb.Mutation) {
Brad Fitzpatrick04f8c522017-04-29 07:39:57 +0000330 if c.verbose {
Brad Fitzpatrick8927fdd2017-03-15 17:51:41 +0000331 log.Printf("mutation: %v", m)
332 }
Kevin Burke9fd5f302017-02-28 13:31:57 -0800333 c.mu.Lock()
334 c.processMutationLocked(m)
Brad Fitzpatrick02c3a362017-11-17 21:48:51 +0000335 c.finishProcessing()
Kevin Burke9fd5f302017-02-28 13:31:57 -0800336 c.mu.Unlock()
Brad Fitzpatrick2ceb7572017-03-25 03:56:50 +0000337
Brad Fitzpatrick04f8c522017-04-29 07:39:57 +0000338 if c.mutationLogger == nil {
Brad Fitzpatrick2ceb7572017-03-25 03:56:50 +0000339 return
340 }
Brad Fitzpatrick04f8c522017-04-29 07:39:57 +0000341 err := c.mutationLogger.Log(m)
Brad Fitzpatrick2ceb7572017-03-25 03:56:50 +0000342 if err != nil {
343 // TODO: handle errors better? failing is only safe option.
344 log.Fatalf("could not log mutation %v: %v\n", m, err)
Kevin Burke9fd5f302017-02-28 13:31:57 -0800345 }
346}
347
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000348// c.mu must be held.
349func (c *Corpus) processMutationLocked(m *maintpb.Mutation) {
350 if im := m.GithubIssue; im != nil {
351 c.processGithubIssueMutation(im)
352 }
Brad Fitzpatrick8cf2b3b2017-03-18 03:48:31 +0000353 if gm := m.Github; gm != nil {
354 c.processGithubMutation(gm)
355 }
Brad Fitzpatricka7d233f2017-03-08 16:56:47 -0800356 if gm := m.Git; gm != nil {
357 c.processGitMutation(gm)
358 }
Kevin Burke92164ac2017-03-18 17:11:31 -0700359 if gm := m.Gerrit; gm != nil {
360 c.processGerritMutation(gm)
361 }
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000362}
363
Brad Fitzpatrickee6321b2017-11-14 23:25:00 +0000364// finishProcessing fixes up invariants and data structures before
365// returning the Corpus from the Update loop back to the user.
366//
367// c.mu must be held.
368func (c *Corpus) finishProcessing() {
369 c.gerrit.finishProcessing()
370}
371
Brad Fitzpatrick8da4ff02017-03-20 22:18:18 +0000372// SyncLoop runs forever (until an error or context expiration) and
373// updates the corpus as the tracked sources change.
374func (c *Corpus) SyncLoop(ctx context.Context) error {
375 return c.sync(ctx, true)
Brad Fitzpatrickb1ddf2b2017-02-08 06:05:26 +0000376}
377
Brad Fitzpatrick8da4ff02017-03-20 22:18:18 +0000378// Sync updates the corpus from its tracked sources.
379func (c *Corpus) Sync(ctx context.Context) error {
380 return c.sync(ctx, false)
Brad Fitzpatrickb1ddf2b2017-02-08 06:05:26 +0000381}
382
Brad Fitzpatrick8da4ff02017-03-20 22:18:18 +0000383func (c *Corpus) sync(ctx context.Context, loop bool) error {
Brad Fitzpatrick73e10fb2017-05-16 17:36:42 -0700384 if _, ok := c.mutationSource.(*netMutSource); ok {
385 return errors.New("maintner: can't run Corpus.Sync on a Corpus using NetworkMutationSource (did you mean Update?)")
386 }
387
Kevin Burke7ebe3f62017-02-20 13:59:38 -0800388 group, ctx := errgroup.WithContext(ctx)
Brad Fitzpatrick8927fdd2017-03-15 17:51:41 +0000389 for _, w := range c.watchedGithubRepos {
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000390 gr, token := w.gr, w.token
Kevin Burke7ebe3f62017-02-20 13:59:38 -0800391 group.Go(func() error {
Brad Fitzpatrick8927fdd2017-03-15 17:51:41 +0000392 log.Printf("Polling %v ...", gr.id)
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000393 for {
394 err := gr.sync(ctx, token, loop)
395 if loop && isTempErr(err) {
396 log.Printf("Temporary error from github %v: %v", gr.ID(), err)
397 time.Sleep(30 * time.Second)
398 continue
399 }
400 log.Printf("github sync ending for %v: %v", gr.ID(), err)
401 return err
402 }
Kevin Burke7ebe3f62017-02-20 13:59:38 -0800403 })
404 }
Brad Fitzpatrick147e8962017-03-08 02:43:47 +0000405 for _, rp := range c.pollGitDirs {
406 rp := rp
407 group.Go(func() error {
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000408 for {
409 err := c.syncGitCommits(ctx, rp, loop)
410 if loop && isTempErr(err) {
411 log.Printf("Temporary error from git repo %v: %v", rp.dir, err)
412 time.Sleep(30 * time.Second)
413 continue
414 }
415 log.Printf("git sync ending for %v: %v", rp.dir, err)
416 return err
417 }
Brad Fitzpatrick147e8962017-03-08 02:43:47 +0000418 })
419 }
Kevin Burke92164ac2017-03-18 17:11:31 -0700420 for _, w := range c.watchedGerritRepos {
Brad Fitzpatrickd97cc622017-03-24 23:23:46 +0000421 gp := w.project
Kevin Burke92164ac2017-03-18 17:11:31 -0700422 group.Go(func() error {
Brad Fitzpatrickd97cc622017-03-24 23:23:46 +0000423 log.Printf("Polling gerrit %v ...", gp.proj)
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000424 for {
425 err := gp.sync(ctx, loop)
426 if loop && isTempErr(err) {
427 log.Printf("Temporary error from gerrit %v: %v", gp.proj, err)
428 time.Sleep(30 * time.Second)
429 continue
430 }
431 log.Printf("gerrit sync ending for %v: %v", gp.proj, err)
432 return err
433 }
Kevin Burke92164ac2017-03-18 17:11:31 -0700434 })
435 }
Kevin Burke7ebe3f62017-02-20 13:59:38 -0800436 return group.Wait()
437}
Brad Fitzpatrick8160f0c2017-04-26 22:52:07 +0000438
439func isTempErr(err error) bool {
440 log.Printf("IS TEMP ERROR? %T %v", err, err)
441 return true
442}