blob: d6a70ebb72a9e07a81fea8e3632b5c4e7bd89685 [file] [log] [blame]
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gerrit
import (
"context"
"encoding/json"
"strconv"
"sync"
"time"
"golang.org/x/oscar/internal/storage"
)
// mergeCacheDuration is how long we cache mergeability information.
//
// Whether a change can be merged is a property of both the change
// and the repo. That is, we can't just recompute it when there is
// activity on a change; it may change due to other activity on the repo.
// To avoid clobbering the Gerrit server, we cache the information.
// This means that the information can be out of date.
// mergeCacheDuration sets a limit on how out of date we let it get.
const mergeCacheDuration = 72 * time.Hour
// mergeCacheTimeType holds the last time we cached merge information
// for any project.
type mergeCacheTimeType struct {
mu sync.Mutex // protects when field
when time.Time
}
// get returns the last time we cached merge information for any project.
// If we have never cached it during this program execution,
// this returns the zero Time.
func (mct *mergeCacheTimeType) get() time.Time {
mct.mu.Lock()
defer mct.mu.Unlock()
return mct.when
}
// set sets the last time we cached merge information for any project.
// This only updates the time if it is newer than the current cache time.
func (mct *mergeCacheTimeType) set(tm time.Time) {
mct.mu.Lock()
defer mct.mu.Unlock()
if mct.when.IsZero() || mct.when.Before(tm) {
mct.when = tm
}
}
// mergeCacheTime is the last time we cached merge information
// for any project.
var mergeCacheTime mergeCacheTimeType
// ChangeMergeable returns whether a change is mergeable.
// If we don't know, it returns true as the safe default.
// The result may be out of date, as it is expensive to compute.
//
// This takes a Context because it may start a background
// goroutine to compute change mergeability; the Context
// will be used for that goroutine.
func (c *Client) ChangeMergeable(ctx context.Context, ch *Change) bool {
if c.divertChanges() {
return c.testClient.isMergeable(c.ChangeNumber(ch))
}
c.computeMergeable(ctx)
changeNum := c.ChangeNumber(ch)
project := c.ChangeProject(ch)
key := o(changeMergeableKind, c.instance, project, changeNum)
val, ok := c.db.Get(key)
if !ok {
// We have no information about this change;
// default to being mergeable.
return true
}
var mergeable bool
if err := json.Unmarshal(val, &mergeable); err != nil {
c.db.Panic("mergeable unmarshal failed", "change", changeNum, "val", val, "err", err)
}
return mergeable
}
// computeMergeable starts a goroutine to recompute mergeability
// for all open changes. The goroutine is only started if it has
// been long enough since the last time we ran the goroutine.
func (c *Client) computeMergeable(ctx context.Context) {
when := mergeCacheTime.get()
if !when.IsZero() && time.Since(when) < mergeCacheDuration {
return
}
go func() {
var lastCheck time.Time
for project := range c.projects() {
projCheck := c.computeMergeableProject(ctx, project)
if lastCheck.IsZero() || projCheck.Before(lastCheck) {
lastCheck = projCheck
}
}
mergeCacheTime.set(lastCheck)
}()
}
// computeMergeableProject recomputes mergeability for the
// changes in a project. It returns the last time the project
// mergeability information was updated.
func (c *Client) computeMergeableProject(ctx context.Context, project string) time.Time {
// Only let one instance recompute mergeability.
key := o(changeMergeableKind, c.instance, project)
skey := string(key)
c.db.Lock(skey)
defer c.db.Unlock(skey)
// Only recompute mergeability if it is out of date.
if val, ok := c.db.Get(key); ok {
var lastUpdate time.Time
if err := json.Unmarshal(val, &lastUpdate); err != nil {
c.db.Panic("gerrit changeMergeable decode", "key", storage.Fmt(key), "val", storage.Fmt(val), "err", err)
}
if time.Since(lastUpdate) < mergeCacheDuration {
return lastUpdate
}
}
b := c.db.Batch()
defer func() {
b.Apply()
c.db.Flush()
}()
for changeNum, changeFn := range c.ChangeNumbers(project) {
if c.ChangeStatus(changeFn()) != "NEW" {
continue
}
c.computeMergeableChange(ctx, b, project, changeNum)
}
now := time.Now()
b.Set(key, storage.JSON(now))
return now
}
// computeMergeableChange recomputes mergeability for a single change.
// This contacts Gerrit to get the current status.
func (c *Client) computeMergeableChange(ctx context.Context, b storage.Batch, project string, changeNum int) {
var mergeable struct {
Mergeable bool `json:"mergeable"`
}
url := "https://" + c.instance + "/changes/" + strconv.Itoa(changeNum) + "/revisions/current/mergeable"
if err := c.get(ctx, url, &mergeable); err != nil {
c.slog.Error("mergeable fetch failed", "change", changeNum, "err", err)
return
}
key := o(changeMergeableKind, c.instance, project, changeNum)
b.Set(key, storage.JSON(mergeable.Mergeable))
b.MaybeApply()
}