blob: 7c70fae323be26e1b9812f5e93de50bdad27722c [file] [log] [blame]
// androidbuild implements a simple interface to look up skia git commit
// hashes from android buildIDs.
package androidbuild
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/errors"
lutil "github.com/syndtr/goleveldb/leveldb/util"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
"go.skia.org/infra/go/vcsinfo"
)
const (
NUM_RETRIES = 10
PAGE_SIZE = 5
SLEEP_DURATION = 3
// TARGETS_KEY is the key for the list of space separated branch:targets we are monitoring.
TARGETS_KEY = "android_ingest_targets"
// LAST_BUILD_FOR_TARGET_TEMPLATE is expanded with the branch and target name to build they key for the last buildID seen for that target.
LAST_BUILD_FOR_TARGET_TEMPLATE = "android_ingest_last_build:%s:%s"
)
// Info is an inferface for querying for commit info from an Android branch, target, and buildID.
type Info interface {
Get(branch, target, buildID string) (*vcsinfo.ShortCommit, error)
}
// info implements Info.
//
// It uses a leveldb database to store information it has already read.
//
// The first time a caller tries to Get(branch, target, buildID) and the
// (branch, target) pair has never been seen before it will be added to
// the list of (branch, target) pairs to periodically refresh. The pairs
// are stored in the leveldb at TARGETS_KEY in the leveldb.
//
// Periodically info will request the build info for all (branch:target)
// pairs it is monitoring. The request will be for all builds from the most
// current back to the last one seen. The last buildID seen for each (branch:target)
// pair is stored at LAST_BUILD_FOR_TARGET_TEMPLATE.
type info struct {
db *leveldb.DB
// commits is an interface for fetching data from the Android Build API,
// broken out as an interface for testability.
commits commits
}
// New creates a new *info as Info.
//
// dir is the directory where the leveldb that caches responses will be written.
// client must be an authenticated client to make requests to the Android Build API.
func New(dir string, client *http.Client) (Info, error) {
db, err := leveldb.OpenFile(dir, nil)
if err != nil && errors.IsCorrupted(err) {
db, err = leveldb.RecoverFile(dir, nil)
}
if err != nil {
return nil, fmt.Errorf("Failed to open leveldb at %s: %s", dir, err)
}
c, err := newAndroidCommits(client)
if err != nil {
return nil, fmt.Errorf("Failed to create commits: %s", err)
}
i := &info{db: db, commits: c}
go i.poll()
return i, nil
}
// Get returns the ShortCommit info for the given branch, target, and buildID.
func (i *info) Get(branch, target, buildID string) (*vcsinfo.ShortCommit, error) {
// Get the list of targets and confirm that this target is in it, otherwise add it to the list of targets.
branchtargets := i.branchtargets()
branchtarget := fmt.Sprintf("%s:%s", branch, target)
if !util.In(branchtarget, branchtargets) {
// If we aren't currently scanning results for this (branch, target) pair
// then add it to the list.
branchtargets = append(branchtargets, branchtarget)
err := i.db.Put([]byte(TARGETS_KEY), []byte(strings.Join(branchtargets, " ")), nil)
if err != nil {
sklog.Errorf("Failed to add new target %s: %s", branchtarget, err)
}
// Always try to fetch the information from the Android Build API directly if
// we don't have it yet.
return i.single_get(branch, target, buildID)
} else {
key, err := toKey(branch, target, buildID)
if err != nil {
return nil, fmt.Errorf("Can't Get with an invalid build ID %q: %s", buildID, err)
}
// Scan backwards through the build info until we find a buildID that is equal to or
// comes before the buildID we are looking for.
iter := i.db.NewIterator(lutil.BytesPrefix([]byte(toPrefix(branch, target))), nil)
defer iter.Release()
if found := iter.Seek([]byte(key)); found {
value := &vcsinfo.ShortCommit{}
if err := json.Unmarshal(iter.Value(), value); err != nil {
return nil, fmt.Errorf("Unable to deserialize value: %s", err)
}
return value, nil
} else {
return i.single_get(branch, target, buildID)
}
}
}
// single_get uses i.commits to attempt to retrieve a single commit, storing the results
// in the leveldb if successful.
func (i *info) single_get(branch, target, buildID string) (*vcsinfo.ShortCommit, error) {
c, err := i.commits.Get(branch, target, buildID)
if err != nil {
return nil, err
}
i.store(branch, target, buildID, c)
return c, nil
}
// branchtargets returns the list of (branch:target)s we are monitoring.
func (i *info) branchtargets() []string {
b, err := i.db.Get([]byte(TARGETS_KEY), nil)
if err != nil {
sklog.Errorf("Failed to get TARGETS_KEY: %s", err)
return []string{}
}
parts := strings.Split(string(b), " ")
ret := []string{}
for _, s := range parts {
if s != "" {
ret = append(ret, s)
}
}
return ret
}
// store the given commit in the leveldb.
func (i *info) store(branch, target, buildID string, commit *vcsinfo.ShortCommit) {
key, err := toKey(branch, target, buildID)
if err != nil {
sklog.Errorf("store: invalid build ID %s: %s", key, err)
return
}
b, err := json.Marshal(commit)
if err != nil {
sklog.Errorf("store: can't encode %#v: %s", commit, err)
return
}
if err := i.db.Put([]byte(key), b, nil); err != nil {
sklog.Errorf("Failed to store commit: %s", err)
}
}
// single_poll does a single loop of polling the API for each target we are monitoring.
func (i *info) single_poll() {
for _, branchtarget := range i.branchtargets() {
parts := strings.Split(branchtarget, ":")
if len(parts) != 2 {
sklog.Errorf("Found an invalid branchtarget: %s", branchtarget)
continue
}
branch := parts[0]
target := parts[1]
// Find the last buildID we've seen so far.
lastBuildID := ""
lastBuildIDKey := []byte(fmt.Sprintf(LAST_BUILD_FOR_TARGET_TEMPLATE, branch, target))
b, err := i.db.Get(lastBuildIDKey, nil)
if err == nil {
lastBuildID = string(b)
}
// Query for commits from latest to lastBuildID.
builds, err := i.commits.List(branch, target, lastBuildID)
if err != nil {
sklog.Errorf("Failed to get commits for %s %s %s: %s", branch, target, lastBuildID, err)
continue
}
// Save each buildID we found.
for k, v := range builds {
i.store(branch, target, k, v)
}
// Now find the largest buildID and store it.
buildIDs := []int{}
for id := range builds {
i, err := strconv.Atoi(id)
if err == nil {
buildIDs = append(buildIDs, i)
}
}
sort.Sort(sort.IntSlice(buildIDs))
if len(buildIDs) > 0 {
lastBuildID = strconv.Itoa(buildIDs[len(buildIDs)-1])
err := i.db.Put(lastBuildIDKey, []byte(lastBuildID), nil)
if err != nil {
sklog.Errorf("Failed to write last build ID: %s", err)
}
}
}
}
// poll periodically polls each target we are monitoring.
func (i *info) poll() {
for range time.Tick(time.Minute) {
i.single_poll()
}
}
// invertBSlice inverts the bytes in a slice so that sorting
// will work in the reverse order.
//
// So leveldb will only sort keys in ascending order, but we want to sort based
// in the buildID in reverse order, so we will invert the bytes so that sorting
// works in the direction that we want.
func invertBSlice(a []byte) []byte {
inverted := make([]byte, len(a))
copy(inverted, a)
for i := 0; i < len(a); i++ {
inverted[i] = 0xff - a[i]
}
return inverted
}
// toKey converts branch, target, and buildID into a string usable as a leveldb key.
//
// For example:
//
// toKey("git_master-skia", "razor-userdebug", "1814540")
//
// Returns
//
// "git_master-skia:razor-userdebug:<inverted bytes of 00000000000001814540>"
func toKey(branch, target, buildID string) ([]byte, error) {
id, err := strconv.Atoi(buildID)
if err != nil {
return []byte{}, err
}
paddedID := []byte(fmt.Sprintf("%020d", id))
return append([]byte(fmt.Sprintf("%s:%s:", branch, target)), invertBSlice(paddedID)...), nil
}
// toPrefix returns a prefix for restricting searches to one particular target in the leveldb.
func toPrefix(branch, target string) []byte {
return []byte(fmt.Sprintf("%s:%s:", branch, target))
}
// fromKey converts a key returned from toKey back into (branch, target, buildID).
func fromKey(key []byte) (string, string, string, error) {
parts := bytes.Split(key, []byte(":"))
if len(parts) != 3 {
return "", "", "", fmt.Errorf("Invalid key format, wrong number of parts: %q", key)
}
id, err := strconv.Atoi(string(invertBSlice(parts[2])))
if err != nil {
return "", "", "", fmt.Errorf("Invalid key format, buildID not a valid int: %q", key)
}
return string(parts[0]), string(parts[1]), strconv.Itoa(id), nil
}