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 }