go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/swarming/server/rpcs/tasks_count_tasks.go (about) 1 // Copyright 2024 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 rpcs 16 17 import ( 18 "context" 19 "strings" 20 21 "google.golang.org/grpc/codes" 22 "google.golang.org/grpc/status" 23 "google.golang.org/protobuf/types/known/timestamppb" 24 25 "go.chromium.org/luci/common/clock" 26 "go.chromium.org/luci/common/logging" 27 "go.chromium.org/luci/gae/service/datastore" 28 29 apipb "go.chromium.org/luci/swarming/proto/api_v2" 30 "go.chromium.org/luci/swarming/server/acls" 31 "go.chromium.org/luci/swarming/server/model" 32 ) 33 34 // CountTasks returns the latest task count for the given request. 35 func (srv *TasksServer) CountTasks(ctx context.Context, req *apipb.TasksCountRequest) (*apipb.TasksCount, error) { 36 if req.Start == nil { 37 return nil, status.Errorf(codes.InvalidArgument, "start timestamp is required") 38 } 39 if req.End == nil { 40 req.End = timestamppb.New(clock.Now(ctx)) 41 } 42 43 // If tags has length 0, tagsFilter and pools will both just be empty. 44 tagsSP := make([]*apipb.StringPair, len(req.Tags)) 45 for i, tag := range req.Tags { 46 parts := strings.SplitN(tag, ":", 2) 47 tagsSP[i] = &apipb.StringPair{Key: parts[0], Value: parts[1]} 48 } 49 tagsFilter, err := model.NewFilter(tagsSP) 50 if err != nil { 51 return nil, status.Errorf(codes.InvalidArgument, "invalid tags: %s", err) 52 } 53 pools := tagsFilter.Pools() 54 55 // If the caller has global permission, they can access all tasks 56 // Otherwise, they are required to provide a pool dimension to check ACL. 57 var res acls.CheckResult 58 state := State(ctx) 59 if len(pools) != 0 { 60 res = state.ACL.CheckAllPoolsPerm(ctx, pools, acls.PermPoolsListTasks) 61 } else { 62 res = state.ACL.CheckServerPerm(ctx, acls.PermPoolsListTasks) 63 } 64 if !res.Permitted { 65 return nil, res.ToGrpcErr() 66 } 67 68 // Limit to the requested time range. An error here means the time range 69 // itself is invalid. 70 query, err := model.FilterTasksByCreationTime(ctx, 71 model.TaskResultSummaryQuery(), 72 req.Start.AsTime(), 73 req.End.AsTime(), 74 ) 75 if err != nil { 76 return nil, status.Errorf(codes.InvalidArgument, "invalid time range: %s", err) 77 } 78 79 // Limit to the requested state, if any. 80 if req.State != apipb.StateQuery_QUERY_ALL { 81 query = model.FilterTasksByState(query, req.State) 82 } 83 84 // Filtering by tags may split the query into multiple queries we'll need to 85 // merge. 86 queries := model.FilterTasksByTags(query, srv.TaskQuerySplitMode, tagsFilter) 87 88 // If we only have one query to run, we can make use of an aggregation query 89 // and utilize the datastore server-side counting (this requires the query 90 // to be eventually consistent). Otherwise, we need to count locally using 91 // datastore.CountMulti(). 92 useAggregation := len(queries) == 1 93 var count int64 94 if useAggregation { 95 count, err = datastore.Count(ctx, queries[0].EventualConsistency(true)) 96 } else { 97 // FirestoreMode ensures all queries are strongly consistent. This allows 98 // them to be used in a transaction, which ensures a more accurate count. 99 for i, q := range queries { 100 queries[i] = q.FirestoreMode(true).EventualConsistency(false) 101 } 102 err = datastore.RunInTransaction(ctx, func(ctx context.Context) error { 103 count, err = datastore.CountMulti(ctx, queries) 104 return err 105 }, &datastore.TransactionOptions{ReadOnly: true}) 106 } 107 108 if err != nil { 109 logging.Errorf(ctx, "Error in TaskResultSummary query: %s", err) 110 return nil, status.Errorf(codes.Internal, "datastore error counting tasks") 111 } 112 113 return &apipb.TasksCount{ 114 Count: int32(count), 115 Now: timestamppb.New(clock.Now(ctx)), 116 }, nil 117 }