go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/invocations/read.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 "context" 19 20 "cloud.google.com/go/spanner" 21 "go.opentelemetry.io/otel/attribute" 22 "google.golang.org/grpc/codes" 23 "google.golang.org/protobuf/proto" 24 "google.golang.org/protobuf/types/known/structpb" 25 26 "go.chromium.org/luci/common/errors" 27 "go.chromium.org/luci/grpc/appstatus" 28 "go.chromium.org/luci/server/span" 29 30 "go.chromium.org/luci/resultdb/internal/spanutil" 31 "go.chromium.org/luci/resultdb/internal/tracing" 32 "go.chromium.org/luci/resultdb/pbutil" 33 pb "go.chromium.org/luci/resultdb/proto/v1" 34 ) 35 36 // ReadColumns reads the specified columns from an invocation Spanner row. 37 // If the invocation does not exist, the returned error is annotated with 38 // NotFound GRPC code. 39 // For ptrMap see ReadRow comment in span/util.go. 40 func ReadColumns(ctx context.Context, id ID, ptrMap map[string]any) error { 41 if id == "" { 42 return errors.Reason("id is unspecified").Err() 43 } 44 err := spanutil.ReadRow(ctx, "Invocations", id.Key(), ptrMap) 45 switch { 46 case spanner.ErrCode(err) == codes.NotFound: 47 return appstatus.Attachf(err, codes.NotFound, "%s not found", id.Name()) 48 49 case err != nil: 50 return errors.Annotate(err, "failed to fetch %s", id.Name()).Err() 51 52 default: 53 return nil 54 } 55 } 56 57 func readMulti(ctx context.Context, ids IDSet, f func(id ID, inv *pb.Invocation) error) error { 58 if len(ids) == 0 { 59 return nil 60 } 61 62 st := spanner.NewStatement(` 63 SELECT 64 i.InvocationId, 65 i.State, 66 i.CreatedBy, 67 i.CreateTime, 68 i.FinalizeTime, 69 i.Deadline, 70 i.Tags, 71 i.BigQueryExports, 72 ARRAY(SELECT IncludedInvocationId FROM IncludedInvocations incl WHERE incl.InvocationID = i.InvocationId), 73 i.ProducerResource, 74 i.Realm, 75 i.Properties, 76 i.Sources, 77 i.InheritSources, 78 i.BaselineId, 79 FROM Invocations i 80 WHERE i.InvocationID IN UNNEST(@invIDs) 81 `) 82 st.Params = spanutil.ToSpannerMap(map[string]any{ 83 "invIDs": ids, 84 }) 85 var b spanutil.Buffer 86 return spanutil.Query(ctx, st, func(row *spanner.Row) error { 87 var id ID 88 included := IDSet{} 89 inv := &pb.Invocation{} 90 91 var ( 92 createdBy spanner.NullString 93 producerResource spanner.NullString 94 realm spanner.NullString 95 properties spanutil.Compressed 96 sources spanutil.Compressed 97 inheritSources spanner.NullBool 98 baselineId spanner.NullString 99 ) 100 err := b.FromSpanner(row, &id, 101 &inv.State, 102 &createdBy, 103 &inv.CreateTime, 104 &inv.FinalizeTime, 105 &inv.Deadline, 106 &inv.Tags, 107 &inv.BigqueryExports, 108 &included, 109 &producerResource, 110 &realm, 111 &properties, 112 &sources, 113 &inheritSources, 114 &baselineId) 115 if err != nil { 116 return err 117 } 118 119 inv.Name = pbutil.InvocationName(string(id)) 120 inv.IncludedInvocations = included.Names() 121 inv.CreatedBy = createdBy.StringVal 122 inv.ProducerResource = producerResource.StringVal 123 inv.Realm = realm.StringVal 124 125 if len(properties) != 0 { 126 inv.Properties = &structpb.Struct{} 127 if err := proto.Unmarshal(properties, inv.Properties); err != nil { 128 return err 129 } 130 } 131 132 if inheritSources.Valid || len(sources) > 0 { 133 inv.SourceSpec = &pb.SourceSpec{} 134 inv.SourceSpec.Inherit = inheritSources.Valid && inheritSources.Bool 135 if len(sources) != 0 { 136 inv.SourceSpec.Sources = &pb.Sources{} 137 if err := proto.Unmarshal(sources, inv.SourceSpec.Sources); err != nil { 138 return err 139 } 140 } 141 } 142 143 if baselineId.Valid { 144 inv.BaselineId = baselineId.StringVal 145 } 146 return f(id, inv) 147 }) 148 } 149 150 // Read reads one invocation from Spanner. 151 // If the invocation does not exist, the returned error is annotated with 152 // NotFound GRPC code. 153 func Read(ctx context.Context, id ID) (*pb.Invocation, error) { 154 var ret *pb.Invocation 155 err := readMulti(ctx, NewIDSet(id), func(id ID, inv *pb.Invocation) error { 156 ret = inv 157 return nil 158 }) 159 160 switch { 161 case err != nil: 162 return nil, err 163 case ret == nil: 164 return nil, appstatus.Errorf(codes.NotFound, "%s not found", id.Name()) 165 default: 166 return ret, nil 167 } 168 } 169 170 // ReadBatch reads multiple invocations from Spanner. 171 // If any of them are not found, returns an error. 172 func ReadBatch(ctx context.Context, ids IDSet) (map[ID]*pb.Invocation, error) { 173 ret := make(map[ID]*pb.Invocation, len(ids)) 174 err := readMulti(ctx, ids, func(id ID, inv *pb.Invocation) error { 175 if _, ok := ret[id]; ok { 176 panic("query is incorrect; it returned duplicated invocation IDs") 177 } 178 ret[id] = inv 179 return nil 180 }) 181 if err != nil { 182 return nil, err 183 } 184 for id := range ids { 185 if _, ok := ret[id]; !ok { 186 return nil, appstatus.Errorf(codes.NotFound, "%s not found", id.Name()) 187 } 188 } 189 return ret, nil 190 } 191 192 // ReadState returns the invocation's state. 193 func ReadState(ctx context.Context, id ID) (pb.Invocation_State, error) { 194 var state pb.Invocation_State 195 err := ReadColumns(ctx, id, map[string]any{"State": &state}) 196 return state, err 197 } 198 199 // ReadStateBatch reads the states of multiple invocations. 200 func ReadStateBatch(ctx context.Context, ids IDSet) (map[ID]pb.Invocation_State, error) { 201 ret := make(map[ID]pb.Invocation_State) 202 err := span.Read(ctx, "Invocations", ids.Keys(), []string{"InvocationID", "State"}).Do(func(r *spanner.Row) error { 203 var id ID 204 var s pb.Invocation_State 205 if err := spanutil.FromSpanner(r, &id, &s); err != nil { 206 return errors.Annotate(err, "failed to fetch %s", ids).Err() 207 } 208 ret[id] = s 209 return nil 210 }) 211 if err != nil { 212 return nil, err 213 } 214 return ret, nil 215 } 216 217 // ReadRealm returns the invocation's realm. 218 func ReadRealm(ctx context.Context, id ID) (string, error) { 219 var realm string 220 err := ReadColumns(ctx, id, map[string]any{"Realm": &realm}) 221 return realm, err 222 } 223 224 // QueryRealms returns the invocations' realms where available from the 225 // Invocations table. 226 // Makes a single RPC. 227 func QueryRealms(ctx context.Context, ids IDSet) (realms map[ID]string, err error) { 228 ctx, ts := tracing.Start(ctx, "resultdb.invocations.QueryRealms", 229 attribute.Int("cr.dev.count", len(ids)), 230 ) 231 defer func() { tracing.End(ts, err) }() 232 233 realms = map[ID]string{} 234 st := spanner.NewStatement(` 235 SELECT 236 i.InvocationId, 237 i.Realm 238 FROM UNNEST(@invIDs) inv 239 JOIN Invocations i 240 ON i.InvocationId = inv`) 241 st.Params = spanutil.ToSpannerMap(map[string]any{ 242 "invIDs": ids, 243 }) 244 b := &spanutil.Buffer{} 245 err = spanutil.Query(ctx, st, func(r *spanner.Row) error { 246 var invocationID ID 247 var realm spanner.NullString 248 if err := b.FromSpanner(r, &invocationID, &realm); err != nil { 249 return err 250 } 251 realms[invocationID] = realm.StringVal 252 return nil 253 }) 254 return realms, err 255 } 256 257 // ReadRealms returns the invocations' realms. 258 // Returns a NotFound error if unable to get the realm for any of the requested 259 // invocations. 260 // Makes a single RPC. 261 func ReadRealms(ctx context.Context, ids IDSet) (realms map[ID]string, err error) { 262 ctx, ts := tracing.Start(ctx, "resultdb.invocations.ReadRealms", 263 attribute.Int("cr.dev.count", len(ids)), 264 ) 265 defer func() { tracing.End(ts, err) }() 266 267 realms, err = QueryRealms(ctx, ids) 268 if err != nil { 269 return nil, err 270 } 271 272 // Return a NotFound error if ret is missing a requested invocation. 273 for id := range ids { 274 if _, ok := realms[id]; !ok { 275 return nil, appstatus.Errorf(codes.NotFound, "%s not found", id.Name()) 276 } 277 } 278 return realms, nil 279 } 280 281 // InclusionKey returns a spanner key for an Inclusion row. 282 func InclusionKey(including, included ID) spanner.Key { 283 return spanner.Key{including.RowID(), included.RowID()} 284 } 285 286 // ReadIncluded reads ids of (directly) included invocations. 287 func ReadIncluded(ctx context.Context, id ID) (IDSet, error) { 288 var ret IDSet 289 var b spanutil.Buffer 290 err := span.Read(ctx, "IncludedInvocations", id.Key().AsPrefix(), []string{"IncludedInvocationId"}).Do(func(row *spanner.Row) error { 291 var included ID 292 if err := b.FromSpanner(row, &included); err != nil { 293 return err 294 } 295 if ret == nil { 296 ret = make(IDSet) 297 } 298 ret.Add(included) 299 return nil 300 }) 301 if err != nil { 302 return nil, err 303 } 304 return ret, nil 305 } 306 307 // ReadSubmitted returns the invocation's submitted status. 308 func ReadSubmitted(ctx context.Context, id ID) (bool, error) { 309 var submitted spanner.NullBool 310 if err := ReadColumns(ctx, id, map[string]any{"Submitted": &submitted}); err != nil { 311 return false, err 312 } 313 // submitted is not a required field and so may be nil, in which we default to false. 314 return submitted.Valid && submitted.Bool, nil 315 }