go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/build/start_options.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 build
    16  
    17  import (
    18  	"reflect"
    19  
    20  	"golang.org/x/time/rate"
    21  	"google.golang.org/protobuf/proto"
    22  	"google.golang.org/protobuf/types/known/structpb"
    23  
    24  	bbpb "go.chromium.org/luci/buildbucket/proto"
    25  	"go.chromium.org/luci/common/sync/dispatcher"
    26  	"go.chromium.org/luci/common/sync/dispatcher/buffer"
    27  	"go.chromium.org/luci/logdog/client/butlerlib/streamclient"
    28  )
    29  
    30  // StartOption is an object which can be passed to the Start function, and
    31  // modifies the behavior of the luciexe/build library.
    32  //
    33  // StartOptions are exclusively constructed from the Opt* functions in this
    34  // package.
    35  //
    36  // StartOptions are all unique per Start (i.e. you can only pass one of a kind
    37  // per option to Start).
    38  type StartOption func(*State)
    39  
    40  // OptLogsink allows you to associate a streamclient with the started build.
    41  //
    42  // See `streamclient.New` and `streamclient.NewFake` for how to create a client
    43  // suitable to your needs (note that this includes a local filesystem option).
    44  //
    45  // If a logsink is configured, it will be used as the output destination for the
    46  // go.chromium.org/luci/common/logging library, and will recieve all data
    47  // written via the Loggable interface.
    48  //
    49  // If no logsink is configured, the go.chromium.org/luci/common/logging library
    50  // will be unaffected, and data written to the Loggable interface will go to
    51  // an ioutil.NopWriteCloser.
    52  func OptLogsink(c *streamclient.Client) StartOption {
    53  	return func(s *State) {
    54  		s.logsink = c
    55  	}
    56  }
    57  
    58  // OptSend allows you to get a callback when the state of the underlying Build
    59  // changes.
    60  //
    61  // This callback will be called at most as frequently as `rate` allows, up to
    62  // once per Build change, and is called with the version number and a copy of
    63  // Build. Only one outstanding invocation of this callback can occur at once.
    64  //
    65  // If new updates come in while this callback is blocking, they will apply
    66  // silently in the background, and as soon as the callback returns (and rate
    67  // allows), it will be invoked again with the current Build state.
    68  //
    69  // Every modification of the Build state increments the version number by one,
    70  // even if it doesn't result in an invocation of the callback. If your program
    71  // modifies the build state from multiple threads, then the version assignment
    72  // is arbitrary, but if you make 10 parallel changes, you'll see the version
    73  // number jump by 10 (and you may, or may not, observe versions in between).
    74  func OptSend(lim rate.Limit, callback func(int64, *bbpb.Build)) StartOption {
    75  	return func(s *State) {
    76  		var err error
    77  		s.sendCh, err = dispatcher.NewChannel(s.ctx, &dispatcher.Options{
    78  			QPSLimit: rate.NewLimiter(lim, 1),
    79  			Buffer: buffer.Options{
    80  				MaxLeases:     1,
    81  				BatchItemsMax: 1,
    82  				FullBehavior: &buffer.DropOldestBatch{
    83  					MaxLiveItems: 1,
    84  				},
    85  			},
    86  		}, func(batch *buffer.Batch) error {
    87  			buildPb, vers := func() (*bbpb.Build, int64) {
    88  				s.buildPbMu.Lock()
    89  				defer s.buildPbMu.Unlock()
    90  
    91  				// Technically we don't need atomic here because copyExclusionMu is held
    92  				// in WRITE mode, but atomic.Int64 is cleaner and aligns on 32-bit ports.
    93  				vers := s.buildPbVers.Load()
    94  
    95  				if s.buildPbVersSent.Load() >= vers {
    96  					return nil, 0
    97  				}
    98  				s.buildPbVersSent.Store(vers)
    99  
   100  				build := proto.Clone(s.buildPb).(*bbpb.Build)
   101  
   102  				// now we populate Output.Properties
   103  				if s.topLevelOutput != nil || len(s.outputProperties) != 0 {
   104  					build.Output.Properties = s.topLevelOutput.getStructClone()
   105  					for ns, child := range s.outputProperties {
   106  						st := child.getStructClone()
   107  						if st == nil {
   108  							continue
   109  						}
   110  						if build.Output.Properties == nil {
   111  							build.Output.Properties, _ = structpb.NewStruct(nil)
   112  						}
   113  						build.Output.Properties.Fields[ns] = structpb.NewStructValue(st)
   114  					}
   115  				}
   116  
   117  				return build, vers
   118  			}()
   119  			if buildPb == nil {
   120  				return nil
   121  			}
   122  
   123  			callback(vers, buildPb)
   124  			return nil
   125  		})
   126  
   127  		if err != nil {
   128  			// This can only happen if Options is malformed.
   129  			// Since it's statically computed above, that's not possible (or the tests
   130  			// are also panicing).
   131  			panic(err)
   132  		}
   133  	}
   134  }
   135  
   136  // OptParseProperties allows you to parse the build's Input.Properties field as
   137  // JSONPB into the given protobuf message.
   138  //
   139  // Message fields which overlap with property namespaces reserved by
   140  // MakePropertyReader will not be populated (i.e. all property namespaces
   141  // reserved with MakePropertyReader will be removed before parsing into this
   142  // message).
   143  //
   144  // Type mismatches (i.e. parsing a non-numeric string into an int field) will
   145  // report an error and quit the build.
   146  //
   147  // Example:
   148  //
   149  //	msg := &MyOutputMessage{}
   150  //	state, ctx := Start(ctx, inputBuild, OptParseProperties(msg))
   151  //	# `msg` has been populated from inputBuild.InputProperties
   152  func OptParseProperties(msg proto.Message) StartOption {
   153  	return func(s *State) {
   154  		s.topLevelInputProperties = msg
   155  	}
   156  }
   157  
   158  // OptStrictInputProperties will cause the build to report an error if data is
   159  // passed via Input.Properties which wasn't parsed into OptParseProperties or
   160  // MakePropertyReader.
   161  func OptStrictInputProperties() StartOption {
   162  	return func(s *State) {
   163  		s.strictParse = true
   164  	}
   165  }
   166  
   167  // OptOutputProperties allows you to register a property writer for the
   168  // top-level output properties of the build.
   169  //
   170  // The registered message must not have any fields which conflict with
   171  // a namespace reserved with MakePropertyModifier, or this panics.
   172  //
   173  // This works like MakePropertyModifier, except that it works at the top level
   174  // (i.e. no namespace) and the functions operate directly on the State (i.e.
   175  // they do not take a context).
   176  //
   177  // Usage
   178  //
   179  //	var writer func(*MyMessage)
   180  //	var merger func(*MyMessage)
   181  //
   182  //	// one function may be nil and will be skipped
   183  //	... = Start(, ..., OptOutputProperties(&writer, &merger))
   184  //
   185  // in go2 this can be improved (possibly by making State a generic type):
   186  func OptOutputProperties(writeFnptr, mergeFnptr any) StartOption {
   187  	writer, merger, msgT := getWriteMergerFnValues(false, writeFnptr, mergeFnptr)
   188  
   189  	return func(s *State) {
   190  		s.topLevelOutput = &outputPropertyState{msg: msgT.New().Interface()}
   191  
   192  		if writer.Kind() == reflect.Func {
   193  			writer.Set(reflect.MakeFunc(writer.Type(), func(args []reflect.Value) []reflect.Value {
   194  				s.excludeCopy(func() bool {
   195  					s.topLevelOutput.set(args[0].Interface().(proto.Message))
   196  					return true
   197  				})
   198  				return nil
   199  			}))
   200  		}
   201  
   202  		if merger.Kind() == reflect.Func {
   203  			merger.Set(reflect.MakeFunc(merger.Type(), func(args []reflect.Value) []reflect.Value {
   204  				s.excludeCopy(func() bool {
   205  					s.topLevelOutput.merge(args[0].Interface().(proto.Message))
   206  					return true
   207  				})
   208  				return nil
   209  			}))
   210  		}
   211  	}
   212  }