go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/invocations/id.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 invocations
    16  
    17  import (
    18  	"crypto/sha256"
    19  	"encoding/hex"
    20  	"fmt"
    21  	"sort"
    22  
    23  	"cloud.google.com/go/spanner"
    24  
    25  	"go.chromium.org/luci/resultdb/internal/spanutil"
    26  	"go.chromium.org/luci/resultdb/pbutil"
    27  )
    28  
    29  // ID can convert an invocation id to various formats.
    30  type ID string
    31  
    32  // ToSpanner implements span.Value.
    33  func (id ID) ToSpanner() any {
    34  	return id.RowID()
    35  }
    36  
    37  // SpannerPtr implements span.Ptr.
    38  func (id *ID) SpannerPtr(b *spanutil.Buffer) any {
    39  	return &b.NullString
    40  }
    41  
    42  // FromSpanner implements span.Ptr.
    43  func (id *ID) FromSpanner(b *spanutil.Buffer) error {
    44  	*id = ""
    45  	if b.NullString.Valid {
    46  		*id = IDFromRowID(b.NullString.StringVal)
    47  	}
    48  	return nil
    49  }
    50  
    51  // MustParseName converts an invocation name to an ID.
    52  // Panics if the name is invalid. Useful for situations when name was already
    53  // validated.
    54  func MustParseName(name string) ID {
    55  	id, err := pbutil.ParseInvocationName(name)
    56  	if err != nil {
    57  		panic(err)
    58  	}
    59  	return ID(id)
    60  }
    61  
    62  // IDFromRowID converts a Spanner-level row ID to an ID.
    63  func IDFromRowID(rowID string) ID {
    64  	return ID(stripHashPrefix(rowID))
    65  }
    66  
    67  // Name returns an invocation name.
    68  func (id ID) Name() string {
    69  	return pbutil.InvocationName(string(id))
    70  }
    71  
    72  // RowID returns an invocation ID used in spanner rows.
    73  // If id is empty, returns "".
    74  func (id ID) RowID() string {
    75  	if id == "" {
    76  		return ""
    77  	}
    78  	return prefixWithHash(string(id))
    79  }
    80  
    81  // Key returns a invocation spanner key.
    82  func (id ID) Key(suffix ...any) spanner.Key {
    83  	ret := make(spanner.Key, 1+len(suffix))
    84  	ret[0] = id.RowID()
    85  	copy(ret[1:], suffix)
    86  	return ret
    87  }
    88  
    89  // IDSet is an unordered set of invocation ids.
    90  type IDSet map[ID]struct{}
    91  
    92  // NewIDSet creates an IDSet from members.
    93  func NewIDSet(ids ...ID) IDSet {
    94  	ret := make(IDSet, len(ids))
    95  	for _, id := range ids {
    96  		ret.Add(id)
    97  	}
    98  	return ret
    99  }
   100  
   101  // Add adds id to the set.
   102  func (s IDSet) Add(id ID) {
   103  	s[id] = struct{}{}
   104  }
   105  
   106  // Union adds other ids.
   107  func (s IDSet) Union(other IDSet) {
   108  	for id := range other {
   109  		s.Add(id)
   110  	}
   111  }
   112  
   113  // RemoveAll removes any ids present in other.
   114  func (s IDSet) RemoveAll(other IDSet) {
   115  	if len(s) > 0 {
   116  		for id := range other {
   117  			s.Remove(id)
   118  		}
   119  	}
   120  }
   121  
   122  // Remove removes id from the set if it was present.
   123  func (s IDSet) Remove(id ID) {
   124  	delete(s, id)
   125  }
   126  
   127  // Has returns true if id is in the set.
   128  func (s IDSet) Has(id ID) bool {
   129  	_, ok := s[id]
   130  	return ok
   131  }
   132  
   133  // String implements fmt.Stringer.
   134  func (s IDSet) String() string {
   135  	strs := make([]string, 0, len(s))
   136  	for id := range s {
   137  		strs = append(strs, string(id))
   138  	}
   139  	sort.Strings(strs)
   140  	return fmt.Sprintf("%q", strs)
   141  }
   142  
   143  // Keys returns a spanner.KeySet.
   144  func (s IDSet) Keys(suffix ...any) spanner.KeySet {
   145  	ret := spanner.KeySets()
   146  	for id := range s {
   147  		ret = spanner.KeySets(id.Key(suffix...), ret)
   148  	}
   149  	return ret
   150  }
   151  
   152  // ToSpanner implements span.Value.
   153  func (s IDSet) ToSpanner() any {
   154  	ret := make([]string, 0, len(s))
   155  	for id := range s {
   156  		ret = append(ret, id.RowID())
   157  	}
   158  	sort.Strings(ret)
   159  	return ret
   160  }
   161  
   162  // SpannerPtr implements span.Ptr.
   163  func (s *IDSet) SpannerPtr(b *spanutil.Buffer) any {
   164  	return &b.StringSlice
   165  }
   166  
   167  // FromSpanner implements span.Ptr.
   168  func (s *IDSet) FromSpanner(b *spanutil.Buffer) error {
   169  	*s = make(IDSet, len(b.StringSlice))
   170  	for _, rowID := range b.StringSlice {
   171  		s.Add(IDFromRowID(rowID))
   172  	}
   173  	return nil
   174  }
   175  
   176  // ParseNames converts invocation names to IDSet.
   177  func ParseNames(names []string) (IDSet, error) {
   178  	ids := make(IDSet, len(names))
   179  	for _, name := range names {
   180  		id, err := pbutil.ParseInvocationName(name)
   181  		if err != nil {
   182  			return nil, err
   183  		}
   184  		ids.Add(ID(id))
   185  	}
   186  	return ids, nil
   187  }
   188  
   189  // MustParseNames converts invocation names to IDSet.
   190  // Panics if a name is invalid. Useful for situations when names were already
   191  // validated.
   192  func MustParseNames(names []string) IDSet {
   193  	ids, err := ParseNames(names)
   194  	if err != nil {
   195  		panic(err)
   196  	}
   197  	return ids
   198  }
   199  
   200  // Names returns a sorted slice of invocation names.
   201  func (s IDSet) Names() []string {
   202  	names := make([]string, 0, len(s))
   203  	for id := range s {
   204  		names = append(names, id.Name())
   205  	}
   206  	sort.Strings(names)
   207  	return names
   208  }
   209  
   210  // SortByRowID returns IDs in the set sorted by row id.
   211  func (s IDSet) SortByRowID() []ID {
   212  	rowIDs := make([]string, 0, len(s))
   213  	for id := range s {
   214  		rowIDs = append(rowIDs, id.RowID())
   215  	}
   216  	sort.Strings(rowIDs)
   217  
   218  	ret := make([]ID, len(rowIDs))
   219  	for i, rowID := range rowIDs {
   220  		ret[i] = ID(stripHashPrefix(rowID))
   221  	}
   222  	return ret
   223  }
   224  
   225  // hashPrefixBytes is the number of bytes of sha256 to prepend to a PK
   226  // to achieve even distribution.
   227  const hashPrefixBytes = 4
   228  
   229  func prefixWithHash(s string) string {
   230  	h := sha256.Sum256([]byte(s))
   231  	prefix := hex.EncodeToString(h[:hashPrefixBytes])
   232  	return fmt.Sprintf("%s:%s", prefix, s)
   233  }
   234  
   235  func stripHashPrefix(s string) string {
   236  	expectedPrefixLen := hex.EncodedLen(hashPrefixBytes) + 1 // +1 for separator
   237  	if len(s) < expectedPrefixLen {
   238  		panic(fmt.Sprintf("%q is too short", s))
   239  	}
   240  	return s[expectedPrefixLen:]
   241  }