github.com/GoogleCloudPlatform/testgrid@v0.0.174/util/gcs/sort.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package gcs
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"sort"
    23  	"sync"
    24  	"time"
    25  
    26  	"cloud.google.com/go/storage"
    27  	"github.com/fvbommel/sortorder"
    28  	"github.com/sirupsen/logrus"
    29  )
    30  
    31  // StatResult contains the result of calling Stat, including any error
    32  type StatResult struct {
    33  	Attrs *storage.ObjectAttrs
    34  	Err   error
    35  }
    36  
    37  // Stat multiple paths using concurrent workers. Result indexes match paths.
    38  func Stat(ctx context.Context, client Stater, workers int, paths ...Path) []StatResult {
    39  	var wg sync.WaitGroup
    40  	wg.Add(workers)
    41  	out := make([]StatResult, len(paths))
    42  	ch := make(chan int)
    43  	for i := 0; i < workers; i++ {
    44  		go func() {
    45  			defer wg.Done()
    46  			for idx := range ch {
    47  				out[idx].Attrs, out[idx].Err = client.Stat(ctx, paths[idx])
    48  			}
    49  		}()
    50  	}
    51  	for idx := range paths {
    52  		ch <- idx
    53  	}
    54  	close(ch)
    55  	wg.Wait()
    56  	return out
    57  }
    58  
    59  // StatExisting reduces Stat() to an array of ObjectAttrs.
    60  //
    61  // Non-existent objects will return a pointer to a zero storage.ObjectAttrs.
    62  // Objects that fail to stat will be nil (and log).
    63  func StatExisting(ctx context.Context, log logrus.FieldLogger, client Stater, paths ...Path) []*storage.ObjectAttrs {
    64  	out := make([]*storage.ObjectAttrs, len(paths))
    65  
    66  	attrs := Stat(ctx, client, 20, paths...)
    67  	for i, attrs := range attrs {
    68  		err := attrs.Err
    69  		switch {
    70  		case attrs.Attrs != nil:
    71  			out[i] = attrs.Attrs
    72  		case errors.Is(err, storage.ErrObjectNotExist):
    73  			out[i] = &storage.ObjectAttrs{}
    74  		default:
    75  			log.WithError(err).WithField("path", paths[i]).Info("Failed to stat")
    76  		}
    77  	}
    78  	return out
    79  }
    80  
    81  // LeastRecentlyUpdated sorts paths by their update timestamp, noting generations and any errors.
    82  func LeastRecentlyUpdated(ctx context.Context, log logrus.FieldLogger, client Stater, paths []Path) map[Path]int64 {
    83  	ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
    84  	defer cancel()
    85  	log.Debug("Sorting groups")
    86  	const workers = 20
    87  	attrs := Stat(ctx, client, workers, paths...)
    88  	updated := make(map[Path]time.Time, len(paths))
    89  	generations := make(map[Path]int64, len(paths))
    90  
    91  	for i, path := range paths {
    92  		attrs, err := attrs[i].Attrs, attrs[i].Err
    93  		switch {
    94  		case err == storage.ErrObjectNotExist:
    95  			generations[path] = 0
    96  		case err != nil:
    97  			log.WithError(err).WithField("path", path).Warning("Stat failed")
    98  			generations[path] = -1
    99  		default:
   100  			updated[path] = attrs.Updated
   101  			generations[path] = attrs.Generation
   102  		}
   103  	}
   104  
   105  	sort.SliceStable(paths, func(i, j int) bool {
   106  		return !updated[paths[i]].After(updated[paths[j]])
   107  	})
   108  
   109  	if n := len(paths) - 1; n > 0 {
   110  		p0 := paths[0]
   111  		pn := paths[n]
   112  		log.WithFields(logrus.Fields{
   113  			"newest-path": pn,
   114  			"newest":      updated[pn],
   115  			"oldest-path": p0,
   116  			"oldest":      updated[p0],
   117  		}).Info("Sorted")
   118  	}
   119  
   120  	return generations
   121  }
   122  
   123  // Touch attempts to win an update of the object.
   124  //
   125  // Cloud copies the current object to itself when the object already exists.
   126  // Otherwise uploads genZero bytes.
   127  func Touch(ctx context.Context, client ConditionalClient, path Path, generation int64, genZero []byte) (*storage.ObjectAttrs, error) {
   128  	var cond storage.Conditions
   129  	if generation != 0 {
   130  		// Attempt to cloud-copy the object to its current location
   131  		// - only 1 will win in a concurrent situation
   132  		// - Increases the last update time.
   133  		cond.GenerationMatch = generation
   134  		return client.If(&cond, &cond).Copy(ctx, path, path)
   135  	}
   136  
   137  	// New group, upload the bytes for this situation.
   138  	cond.DoesNotExist = true
   139  	return client.If(&cond, &cond).Upload(ctx, path, genZero, DefaultACL, NoCache)
   140  }
   141  
   142  // Sort the builds by monotonically decreasing original prefix base name.
   143  //
   144  // In other words,
   145  //   gs://b/1
   146  //   gs://a/5
   147  //   gs://c/10
   148  // becomes:
   149  //   gs://c/10
   150  //   gs://a/5
   151  //   gs://b/1
   152  func Sort(builds []Build) {
   153  	sort.SliceStable(builds, func(i, j int) bool { // greater
   154  		return !sortorder.NaturalLess(builds[i].baseName, builds[j].baseName) && builds[i].baseName != builds[j].baseName
   155  	})
   156  }