diff options
Diffstat (limited to 'third_party/gerrit-queue/submitqueue/runner.go')
-rw-r--r-- | third_party/gerrit-queue/submitqueue/runner.go | 220 |
1 files changed, 220 insertions, 0 deletions
diff --git a/third_party/gerrit-queue/submitqueue/runner.go b/third_party/gerrit-queue/submitqueue/runner.go new file mode 100644 index 000000000000..0b4cbcd8dd7c --- /dev/null +++ b/third_party/gerrit-queue/submitqueue/runner.go @@ -0,0 +1,220 @@ +package submitqueue + +import ( + "fmt" + "sync" + + "github.com/apex/log" + + "github.com/tweag/gerrit-queue/gerrit" +) + +// Runner is a struct existing across the lifetime of a single run of the submit queue +// it contains a mutex to avoid being run multiple times. +// In fact, it even cancels runs while another one is still in progress. +// It contains a Gerrit object facilitating access, a log object, the configured submit queue tag +// and a `wipSerie` (only populated if waiting for a rebase) +type Runner struct { + mut sync.Mutex + currentlyRunning bool + wipSerie *gerrit.Serie + logger *log.Logger + gerrit *gerrit.Client +} + +// NewRunner creates a new Runner struct +func NewRunner(logger *log.Logger, gerrit *gerrit.Client) *Runner { + return &Runner{ + logger: logger, + gerrit: gerrit, + } +} + +// isAutoSubmittable determines if something could be autosubmitted, potentially requiring a rebase +// for this, it needs to: +// * have the "Autosubmit" label set to +1 +// * have gerrit's 'submittable' field set to true +// it doesn't check if the series is rebased on HEAD +func (r *Runner) isAutoSubmittable(s *gerrit.Serie) bool { + for _, c := range s.ChangeSets { + if c.Submittable != true || !c.IsAutosubmit() { + return false + } + } + return true +} + +// IsCurrentlyRunning returns true if the runner is currently running +func (r *Runner) IsCurrentlyRunning() bool { + return r.currentlyRunning +} + +// GetWIPSerie returns the current wipSerie, if any, nil otherwiese +// Acquires a lock, so check with IsCurrentlyRunning first +func (r *Runner) GetWIPSerie() *gerrit.Serie { + r.mut.Lock() + defer func() { + r.mut.Unlock() + }() + return r.wipSerie +} + +// Trigger gets triggered periodically +func (r *Runner) Trigger(fetchOnly bool) error { + // TODO: If CI fails, remove the auto-submit labels => rules.pl + // Only one trigger can run at the same time + r.mut.Lock() + if r.currentlyRunning { + return fmt.Errorf("Already running, skipping") + } + r.currentlyRunning = true + r.mut.Unlock() + defer func() { + r.mut.Lock() + r.currentlyRunning = false + r.mut.Unlock() + }() + + // Prepare the work by creating a local cache of gerrit state + err := r.gerrit.Refresh() + if err != nil { + return err + } + + // early return if we only want to fetch + if fetchOnly { + return nil + } + + if r.wipSerie != nil { + // refresh wipSerie with how it looks like in gerrit now + wipSerie := r.gerrit.FindSerie(func(s *gerrit.Serie) bool { + // the new wipSerie needs to have the same number of changesets + if len(r.wipSerie.ChangeSets) != len(s.ChangeSets) { + return false + } + // … and the same ChangeIDs. + for idx, c := range s.ChangeSets { + if r.wipSerie.ChangeSets[idx].ChangeID != c.ChangeID { + return false + } + } + return true + }) + if wipSerie == nil { + r.logger.WithField("wipSerie", r.wipSerie).Warn("wipSerie has disappeared") + r.wipSerie = nil + } else { + r.wipSerie = wipSerie + } + } + + for { + // initialize logger + r.logger.Info("Running") + if r.wipSerie != nil { + // if we have a wipSerie + l := r.logger.WithField("wipSerie", r.wipSerie) + l.Info("Checking wipSerie") + + // discard wipSerie not rebased on HEAD + // we rebase them at the end of the loop, so this means master advanced without going through the submit queue + if !r.gerrit.SerieIsRebasedOnHEAD(r.wipSerie) { + l.Warnf("HEAD has moved to %v while still waiting for wipSerie, discarding it", r.gerrit.GetHEAD()) + r.wipSerie = nil + continue + } + + // we now need to check CI feedback: + // wipSerie might have failed CI in the meantime + for _, c := range r.wipSerie.ChangeSets { + if c == nil { + l.Error("BUG: changeset is nil") + continue + } + if c.Verified < 0 { + l.WithField("failingChangeset", c).Warnf("wipSerie failed CI in the meantime, discarding.") + r.wipSerie = nil + continue + } + } + + // it might still be waiting for CI + for _, c := range r.wipSerie.ChangeSets { + if c == nil { + l.Error("BUG: changeset is nil") + continue + } + if c.Verified == 0 { + l.WithField("pendingChangeset", c).Warnf("still waiting for CI feedback in wipSerie, going back to sleep.") + // break the loop, take a look at it at the next trigger. + return nil + } + } + + // it might be autosubmittable + if r.isAutoSubmittable(r.wipSerie) { + l.Infof("submitting wipSerie") + // if the WIP changeset is ready (auto submittable and rebased on HEAD), submit + for _, changeset := range r.wipSerie.ChangeSets { + _, err := r.gerrit.SubmitChangeset(changeset) + if err != nil { + l.WithField("changeset", changeset).Error("error submitting changeset") + r.wipSerie = nil + return err + } + } + r.wipSerie = nil + } else { + l.Error("BUG: wipSerie is not autosubmittable") + r.wipSerie = nil + } + } + + r.logger.Info("Looking for series ready to submit") + // Find serie, that: + // * has the auto-submit label + // * has +2 review + // * has +1 CI + // * is rebased on master + serie := r.gerrit.FindSerie(func(s *gerrit.Serie) bool { + return r.isAutoSubmittable(s) && s.ChangeSets[0].ParentCommitIDs[0] == r.gerrit.GetHEAD() + }) + if serie != nil { + r.logger.WithField("serie", serie).Info("Found serie to submit without necessary rebase") + r.wipSerie = serie + continue + } + + // Find serie, that: + // * has the auto-submit label + // * has +2 review + // * has +1 CI + // * is NOT rebased on master + serie = r.gerrit.FindSerie(r.isAutoSubmittable) + if serie == nil { + r.logger.Info("no more submittable series found, going back to sleep.") + break + } + + l := r.logger.WithField("serie", serie) + l.Info("found serie, which needs a rebase") + // TODO: move into Client.RebaseSeries function + head := r.gerrit.GetHEAD() + for _, changeset := range serie.ChangeSets { + changeset, err := r.gerrit.RebaseChangeset(changeset, head) + if err != nil { + l.Error(err.Error()) + return err + } + head = changeset.CommitID + } + // we don't need to care about updating the rebased changesets or getting the updated HEAD, + // as we'll refetch it on the beginning of the next trigger anyways + r.wipSerie = serie + break + } + + r.logger.Info("Run complete") + return nil +} |