go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/invocations/graph/traverse.go (about)

     1  // Copyright 2023 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 graph contains methods to explore reachable invocations.
    16  package graph
    17  
    18  import (
    19  	"context"
    20  	"sync"
    21  
    22  	"cloud.google.com/go/spanner"
    23  	"go.chromium.org/luci/resultdb/internal/invocations"
    24  	"go.chromium.org/luci/resultdb/internal/spanutil"
    25  	"go.chromium.org/luci/server/span"
    26  	"golang.org/x/sync/errgroup"
    27  	"golang.org/x/sync/semaphore"
    28  )
    29  
    30  // FindInheritSourcesDescendants finds and return all invocations (including the root)
    31  // which inherit commit source information from the root.
    32  func FindInheritSourcesDescendants(ctx context.Context, invID invocations.ID) (invocations.IDSet, error) {
    33  	ctx, cancel := span.ReadOnlyTransaction(ctx)
    34  	defer cancel()
    35  
    36  	// Walk the graph of invocations, starting from the root, along the inclusion
    37  	// edges.
    38  	seen := make(invocations.IDSet, 1)
    39  	var mu sync.Mutex
    40  
    41  	// Limit the number of concurrent queries.
    42  	sem := semaphore.NewWeighted(64)
    43  
    44  	eg, ctx := errgroup.WithContext(ctx)
    45  
    46  	var visit func(id invocations.ID)
    47  	visit = func(id invocations.ID) {
    48  		// Do not visit same node twice.
    49  		mu.Lock()
    50  		if seen.Has(id) {
    51  			mu.Unlock()
    52  			return
    53  		}
    54  		seen.Add(id)
    55  		mu.Unlock()
    56  
    57  		// Concurrently fetch inclusions without a lock.
    58  		eg.Go(func() error {
    59  			// Limit concurrent Spanner queries.
    60  			if err := sem.Acquire(ctx, 1); err != nil {
    61  				return err
    62  			}
    63  			defer sem.Release(1)
    64  
    65  			st := spanner.NewStatement(`
    66  				SELECT included.InvocationId
    67  				FROM IncludedInvocations incl
    68  				JOIN Invocations included on incl.IncludedInvocationId = included.InvocationId
    69  				WHERE incl.InvocationId = @invID AND included.InheritSources
    70  			`)
    71  			st.Params = spanutil.ToSpannerMap(map[string]any{"invID": id})
    72  			var b spanutil.Buffer
    73  			return span.Query(ctx, st).Do(func(row *spanner.Row) error {
    74  				var includedID invocations.ID
    75  				if err := b.FromSpanner(row, &includedID); err != nil {
    76  					return err
    77  				}
    78  				visit(includedID)
    79  				return nil
    80  			})
    81  		})
    82  	}
    83  
    84  	visit(invID)
    85  
    86  	if err := eg.Wait(); err != nil {
    87  		return nil, err
    88  	}
    89  	return seen, nil
    90  }