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 }