cmd/gopherbot: table-ify gopherbot tasks, start of daemon mode

Change-Id: I147a806c00e4c0de66b4ae490d838d7a3ac42daf
Reviewed-on: https://go-review.googlesource.com/40971
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/cmd/gopherbot/gopherbot.go b/cmd/gopherbot/gopherbot.go
index b976519..d412ecd 100644
--- a/cmd/gopherbot/gopherbot.go
+++ b/cmd/gopherbot/gopherbot.go
@@ -28,6 +28,7 @@
 
 var (
 	dryRun = flag.Bool("dry-run", false, "just report what would've been done, without changing anything")
+	daemon = flag.Bool("daemon", false, "run in daemon mode")
 )
 
 func getGithubToken() (string, error) {
@@ -82,56 +83,29 @@
 		gorepo: repo,
 	}
 
-	var fail bool
-
-	if err := bot.freezeOldIssues(ctx); err != nil {
-		log.Printf("freezing old issues: %v", err)
-		fail = true
-	}
-
-	if err := bot.labelProposals(ctx); err != nil {
-		log.Printf("labeling proposals: %v", err)
-		fail = true
-	}
-
-	if err := bot.setSubrepoMilestones(ctx); err != nil {
-		log.Printf("setting subrepo milestones: %v", err)
-		fail = true
-	}
-
-	if err := bot.setGccgoMilestones(ctx); err != nil {
-		log.Printf("setting gccgo milestones: %v", err)
-		fail = true
-	}
-
-	if err := bot.labelBuildIssues(ctx); err != nil {
-		log.Printf("labeling build issues: %v", err)
-		fail = true
-	}
-
-	if err := bot.labelDocumentationIssues(ctx); err != nil {
-		log.Printf("labeling documentation issues: %v", err)
-		fail = true
-	}
-
-	if err := bot.closeStaleWaitingForInfo(ctx); err != nil {
-		log.Printf("closing stale WaitingForInfo: %v", err)
-		fail = true
-	}
-
-	// "CL nnnn mentions this issue"
-	if err := bot.cl2issue(ctx); err != nil {
-		log.Printf("cl2issue: %v", err)
-		fail = true
-	}
-
-	if err := bot.checkCherryPicks(ctx); err != nil {
-		log.Printf("checking cherry picks: %v", err)
-		fail = true
-	}
-
-	if fail {
-		os.Exit(1)
+	for {
+		var nextLoop time.Time
+		err := bot.doTasks(ctx)
+		if err != nil {
+			log.Print(err)
+			nextLoop = time.Now().Add(30 * time.Second)
+		}
+		if !*daemon {
+			if err != nil {
+				os.Exit(1)
+			}
+			return
+		}
+		// TODO: if err != nil, pass a ctx with 30s timeout and retry the doTasks.
+		// Maybe use a better ctx above too.
+		if err := corpus.Update(ctx); err != nil {
+			log.Fatalf("corpus.Update: %v", err)
+		}
+		if nextLoop.After(time.Now()) {
+			sleep := time.Until(nextLoop)
+			log.Printf("Sleeping for %v after previous error.", sleep)
+			time.Sleep(sleep)
+		}
 	}
 }
 
@@ -141,6 +115,31 @@
 	gorepo *maintner.GitHubRepo
 }
 
+var tasks = []struct {
+	name string
+	fn   func(*gopherbot, context.Context) error
+}{
+	{"freeze old issues", (*gopherbot).freezeOldIssues},
+	{"label proposals", (*gopherbot).labelProposals},
+	{"set subrepo milestones", (*gopherbot).setSubrepoMilestones},
+	{"set gccgo milestones", (*gopherbot).setGccgoMilestones},
+	{"label build issues", (*gopherbot).labelBuildIssues},
+	{"label documentation issues", (*gopherbot).labelDocumentationIssues},
+	{"close stale WaitingForInfo", (*gopherbot).closeStaleWaitingForInfo},
+	{"cl2issue", (*gopherbot).cl2issue},
+	{"check cherry picks", (*gopherbot).checkCherryPicks},
+}
+
+func (b *gopherbot) doTasks(ctx context.Context) error {
+	for _, task := range tasks {
+		if err := task.fn(b, ctx); err != nil {
+			log.Printf("%s: %v", task.name, err)
+			return err
+		}
+	}
+	return nil
+}
+
 func (b *gopherbot) addLabel(ctx context.Context, gi *maintner.GitHubIssue, label string) error {
 	_, _, err := b.ghc.Issues.AddLabelsToIssue(ctx, "golang", "go", int(gi.Number), []string{label})
 	return err
@@ -467,6 +466,10 @@
 					return nil
 				})
 				if !hasComment {
+					printIssue("cl2issue", gi)
+					if *dryRun {
+						return nil
+					}
 					if err := b.addGitHubComment(ctx, "golang", "go", gi.Number, fmt.Sprintf("CL https://golang.org/cl/%d mentions this issue.", cl.Number)); err != nil {
 						return err
 					}
diff --git a/maintner/maintner.go b/maintner/maintner.go
index 0d3cd87..e3ac6bb 100644
--- a/maintner/maintner.go
+++ b/maintner/maintner.go
@@ -156,7 +156,9 @@
 	GetMutations(context.Context) <-chan *maintpb.Mutation
 }
 
-// Initialize populates the Corpus using the data from the MutationSource.
+// Initialize populates the Corpus using the data from the
+// MutationSource. It returns once it's up-to-date. To incrementally
+// update it later, use the Update method.
 func (c *Corpus) Initialize(ctx context.Context, src MutationSource) error {
 	ch := src.GetMutations(ctx)
 	done := ctx.Done()
@@ -180,6 +182,14 @@
 	}
 }
 
+// Update incrementally updates the corpus from its current state to
+// the latest state from the MutationSource passed earlier to
+// Initialize. It does not return until there's either a new change or
+// the context expires.
+func (c *Corpus) Update(ctx context.Context) error {
+	panic("TODO")
+}
+
 // addMutation adds a mutation to the log and immediately processes it.
 func (c *Corpus) addMutation(m *maintpb.Mutation) {
 	if c.Verbose {