go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/model/tagindex.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package model
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"time"
    21  
    22  	"go.chromium.org/luci/buildbucket/protoutil"
    23  	"go.chromium.org/luci/common/data/rand/mathrand"
    24  	"go.chromium.org/luci/common/errors"
    25  	"go.chromium.org/luci/common/logging"
    26  	"go.chromium.org/luci/gae/service/datastore"
    27  )
    28  
    29  // Ensure TagIndexEntry implements datastore.PropertyConverter.
    30  var _ datastore.PropertyConverter = &TagIndexEntry{}
    31  
    32  // TagIndexEntry refers to a particular Build entity.
    33  type TagIndexEntry struct {
    34  	// BuildID is the ID of the Build entity this entry refers to.
    35  	BuildID int64 `json:"build_id"`
    36  	// <project>/<bucket>. Bucket is in v2 format.
    37  	// e.g. chromium/try (never chromium/luci.chromium.try).
    38  	BucketID string `json:"bucket_id"`
    39  	// CreatedTime is the time this entry was created.
    40  	CreatedTime time.Time `json:"created_time"`
    41  }
    42  
    43  // FromProperty deserializes TagIndexEntries from the datastore.
    44  // Implements datastore.PropertyConverter.
    45  func (e *TagIndexEntry) FromProperty(p datastore.Property) error {
    46  	for key, val := range p.Value().(datastore.PropertyMap) {
    47  		switch key {
    48  		case "build_id":
    49  			e.BuildID = val.Slice()[0].Value().(int64)
    50  		case "bucket_id":
    51  			e.BucketID = val.Slice()[0].Value().(string)
    52  		case "created_time":
    53  			e.CreatedTime = val.Slice()[0].Value().(time.Time)
    54  		}
    55  	}
    56  	return nil
    57  }
    58  
    59  // ToProperty serializes TagIndexEntries to datastore format.
    60  // Implements datastore.PropertyConverter.
    61  func (e *TagIndexEntry) ToProperty() (datastore.Property, error) {
    62  	p := datastore.Property{}
    63  	err := p.SetValue(datastore.PropertyMap{
    64  		"build_id":     datastore.MkProperty(e.BuildID),
    65  		"bucket_id":    datastore.MkProperty(e.BucketID),
    66  		"created_time": datastore.MkProperty(e.CreatedTime),
    67  	}, datastore.NoIndex)
    68  	return p, err
    69  }
    70  
    71  // MaxTagIndexEntries is the maximum number of entries that may be associated
    72  // with a single TagIndex entity.
    73  const MaxTagIndexEntries = 1000
    74  
    75  // TagIndexShardCount is the number of shards used by the TagIndex.
    76  const TagIndexShardCount = 16
    77  
    78  // TagIndex is an index used to search Build entities by tag.
    79  type TagIndex struct {
    80  	_kind string `gae:"$kind,TagIndex"`
    81  	// ID is a "<key>:<value>" or ":<index>:<key>:<value>" string for index > 0.
    82  	ID string `gae:"$id"`
    83  	// Incomplete means there are more than MaxTagIndexEntries entities
    84  	// with the same ID, and therefore the index is incomplete and cannot be
    85  	// searched.
    86  	Incomplete bool `gae:"permanently_incomplete,noindex"`
    87  	// Entries is a slice of TagIndexEntries matching this ID.
    88  	Entries []TagIndexEntry `gae:"entries,noindex"`
    89  }
    90  
    91  // TagIndexIncomplete means the tag index is incomplete and thus cannot be searched.
    92  var TagIndexIncomplete = errors.BoolTag{Key: errors.NewTagKey("tag index incomplete")}
    93  
    94  // SearchTagIndex searches the tag index for the given tag.
    95  // Returns an error tagged with TagIndexIncomplete if the tag index is
    96  // incomplete and thus cannot be searched.
    97  func SearchTagIndex(ctx context.Context, key, val string) ([]*TagIndexEntry, error) {
    98  	shds := make([]TagIndex, TagIndexShardCount)
    99  	for i := range shds {
   100  		if i == 0 {
   101  			shds[i].ID = fmt.Sprintf("%s:%s", key, val)
   102  		} else {
   103  			shds[i].ID = fmt.Sprintf(":%d:%s:%s", i, key, val)
   104  		}
   105  	}
   106  	if err := GetIgnoreMissing(ctx, shds); err != nil {
   107  		return nil, errors.Annotate(err, "error fetching tag index for %q", fmt.Sprintf("%s:%s", key, val)).Err()
   108  	}
   109  	var ents []*TagIndexEntry
   110  	for _, s := range shds {
   111  		if s.Incomplete {
   112  			return nil, errors.Reason("tag index incomplete for %q", fmt.Sprintf("%s:%s", key, val)).Tag(TagIndexIncomplete).Err()
   113  		}
   114  		for i := range s.Entries {
   115  			// check in case the tagIndexEntry is corrupted.
   116  			if _, _, err := protoutil.ParseBucketID(s.Entries[i].BucketID); err != nil {
   117  				logging.Warningf(ctx, "Bad TagIndexEntry(%+v): %s in TagIndex %s", s.Entries[i], err, s.ID)
   118  				continue
   119  			}
   120  			ents = append(ents, &s.Entries[i])
   121  		}
   122  	}
   123  	return ents, nil
   124  }
   125  
   126  // UpdateTagIndex updates the tag index for the given tag.
   127  func UpdateTagIndex(ctx context.Context, tag string, ents []TagIndexEntry) error {
   128  	if len(ents) == 0 {
   129  		return nil
   130  	}
   131  	return updateTagIndex(ctx, tag, mathrand.Intn(ctx, TagIndexShardCount), ents)
   132  }
   133  
   134  // updateTagIndex updates the tag index's specified shard for the given tag.
   135  func updateTagIndex(ctx context.Context, tag string, shard int, ents []TagIndexEntry) error {
   136  	if len(ents) == 0 {
   137  		return nil
   138  	}
   139  	return datastore.RunInTransaction(ctx, func(ctx context.Context) error {
   140  		shd := &TagIndex{
   141  			ID: tag,
   142  		}
   143  		if shard > 0 {
   144  			shd.ID = fmt.Sprintf(":%d:%s", shard, tag)
   145  		}
   146  		switch err := datastore.Get(ctx, shd); {
   147  		case err == datastore.ErrNoSuchEntity:
   148  		case err != nil:
   149  			return errors.Annotate(err, "error fetching tag index for %q", shd.ID).Err()
   150  		case shd.Incomplete:
   151  			// No point in updating an incomplete index because it cannot be searched.
   152  			return nil
   153  		}
   154  
   155  		orig := len(shd.Entries)
   156  		shd.Entries = append(shd.Entries, ents...)
   157  		if len(shd.Entries) > MaxTagIndexEntries {
   158  			shd.Entries = nil
   159  			shd.Incomplete = true
   160  			logging.Warningf(ctx, "marking tag index incomplete for %q", shd.ID)
   161  		} else {
   162  			logging.Debugf(ctx, "updating tag index for %q (entries %d -> %d)", shd.ID, orig, len(ents))
   163  		}
   164  
   165  		if err := datastore.Put(ctx, shd); err != nil {
   166  			return errors.Annotate(err, "error updating tag index for %q", shd.ID).Err()
   167  		}
   168  		return nil
   169  	}, nil)
   170  }