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  }