go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/build/state.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 "context" 19 "fmt" 20 "io" 21 "sync" 22 "sync/atomic" 23 24 "google.golang.org/protobuf/proto" 25 "google.golang.org/protobuf/types/known/timestamppb" 26 27 bbpb "go.chromium.org/luci/buildbucket/proto" 28 "go.chromium.org/luci/buildbucket/protoutil" 29 "go.chromium.org/luci/common/clock" 30 "go.chromium.org/luci/common/errors" 31 "go.chromium.org/luci/common/iotools" 32 "go.chromium.org/luci/common/logging" 33 "go.chromium.org/luci/common/sync/dispatcher" 34 "go.chromium.org/luci/logdog/client/butlerlib/streamclient" 35 ldTypes "go.chromium.org/luci/logdog/common/types" 36 ) 37 38 // State is the state of the current Build. 39 // 40 // This is properly initialized with the Start function, and as long as it isn't 41 // "End"ed, you can manipulate it with the State's various methods. 42 // 43 // The State is preserved in the context.Context for use with the ScheduleStep 44 // and StartStep functions. These will add a new manipulatable step to the build 45 // State. 46 // 47 // All manipulations to the build State will result in an invocation of the 48 // configured Send function (see OptSend). 49 type State struct { 50 ctx context.Context 51 ctxCloser func() 52 53 // inputBuildPb represents the build when state was created. 54 // This is used to provide client access to input build. 55 inputBuildPb *bbpb.Build 56 // buildPbMu is held in "WRITE" mode whenever buildPb may be directly written 57 // to, or in order to do `proto.Clone` on buildPb (since the Clone operation 58 // actually can write metadata to the struct), and is not safe with concurrent 59 // writes to the proto message. 60 // 61 // buildPbMu is held in "READ" mode for all other reads of the buildPb; The 62 // library has other mutexes to protect indivitual portions of the buildPb 63 // from concurrent modification. 64 // 65 // This is done to allow e.g. multiple Steps to be mutated concurrently, but 66 // allow `proto.Clone` to proceed safely. 67 buildPbMu sync.RWMutex 68 // buildPb represents the live build. 69 buildPb *bbpb.Build 70 // buildPbVers updated/read while holding buildPbMu in either WRITE/READ mode. 71 buildPbVers atomic.Int64 72 // buildPbVersSent only updated when buildPbMu is held in WRITE mode. 73 buildPbVersSent atomic.Int64 74 75 sendCh dispatcher.Channel 76 77 logsink *streamclient.Client 78 logNames nameTracker 79 logClosers map[string]func() error 80 81 strictParse bool 82 83 reservedInputProperties map[string]proto.Message 84 topLevelInputProperties proto.Message 85 86 // Note that outputProperties is statically allocated at Start time; No keys 87 // are added/removed for the duration of the Build. 88 outputProperties map[string]*outputPropertyState 89 topLevelOutput *outputPropertyState 90 91 stepNames nameTracker 92 } 93 94 // newState creates a new state. 95 func newState(inputBuildPb *bbpb.Build, logClosers map[string]func() error, outputProperties map[string]*outputPropertyState) *State { 96 state := &State{ 97 buildPb: inputBuildPb, 98 logClosers: logClosers, 99 outputProperties: outputProperties, 100 } 101 102 if inputBuildPb != nil { 103 state.inputBuildPb = proto.Clone(inputBuildPb).(*bbpb.Build) 104 } 105 106 return state 107 } 108 109 var _ Loggable = (*State)(nil) 110 111 // End sets the build's final status, according to `err` (See ExtractStatus). 112 // 113 // End will also be able to set INFRA_FAILURE status and log additional 114 // information if the program is panic'ing. 115 // 116 // End must be invoked like: 117 // 118 // var err error 119 // state, ctx := build.Start(ctx, initialBuild, ...) 120 // defer func() { state.End(err) }() 121 // 122 // err = opThatErrsOrPanics(ctx) 123 // 124 // NOTE: A panic will still crash the program as usual. This does NOT 125 // `recover()` the panic. Please use conventional Go error handling and control 126 // flow mechanisms. 127 func (s *State) End(err error) { 128 var message string 129 s.mutate(func() bool { 130 s.buildPb.Output.Status, message = computePanicStatus(err) 131 s.buildPb.Status = s.buildPb.Output.Status 132 s.buildPb.EndTime = timestamppb.New(clock.Now(s.ctx)) 133 134 for logName, closer := range s.logClosers { 135 if err := closer(); err != nil { 136 logging.Warningf(s.ctx, "error closing log %q: %s", logName, err) 137 } 138 } 139 s.logClosers = nil 140 141 return true 142 }) 143 // buildPb is immutable after mutate ends, so we should be fine to access it 144 // outside the locks. 145 146 if s.sendCh.C != nil { 147 s.sendCh.CloseAndDrain(s.ctx) 148 } 149 150 if s.logsink == nil || s.buildPb.Output.Status != bbpb.Status_SUCCESS { 151 // If we're panicking, we need to log. In a situation where we have a log 152 // sink (i.e. a real build), all other information is already reflected via 153 // the Build message itself. 154 logStatus(s.ctx, s.buildPb.Output.Status, message, s.buildPb.SummaryMarkdown) 155 } 156 157 s.ctxCloser() 158 } 159 160 // addLog adds a new Log entry to this Step. 161 // 162 // `name` is the user-provided name for the log. 163 // 164 // `openStream` is a callback which takes 165 // - `dedupedName` - the deduplicated version of `name` 166 // - `relLdName` - The logdog stream name, relative to this process' 167 // LOGDOG_NAMESPACE, suitable for use with s.state.logsink. 168 func (s *State) addLog(name string, openStream func(dedupedName string, relLdName ldTypes.StreamName) io.Closer) *bbpb.Log { 169 var logRef *bbpb.Log 170 s.mutate(func() bool { 171 name = s.logNames.resolveName(name) 172 relLdName := fmt.Sprintf("log/%d", len(s.buildPb.Output.Logs)) 173 logRef = &bbpb.Log{ 174 Name: name, 175 Url: relLdName, 176 } 177 s.buildPb.Output.Logs = append(s.buildPb.Output.Logs, logRef) 178 if closer := openStream(name, ldTypes.StreamName(relLdName)); closer != nil { 179 s.logClosers[relLdName] = closer.Close 180 } 181 return true 182 }) 183 return logRef 184 } 185 186 // Log creates a new step-level line-oriented text log stream with the given name. 187 // Returns a Log value which can be written to directly, but also provides additional 188 // information about the log itself. 189 // 190 // The stream will close when the state is End'd. 191 func (s *State) Log(name string, opts ...streamclient.Option) *Log { 192 if ls := s.logsink; ls != nil { 193 var ret io.WriteCloser 194 logRef := s.addLog(name, func(name string, relLdName ldTypes.StreamName) io.Closer { 195 var err error 196 ret, err = ls.NewStream(s.ctx, relLdName, opts...) 197 if err != nil { 198 panic(err) 199 } 200 return ret 201 }) 202 var infra *bbpb.BuildInfra_LogDog 203 if b := s.Build(); b != nil { 204 infra = b.GetInfra().GetLogdog() 205 } 206 return &Log{ 207 Writer: ret, 208 ref: logRef, 209 namespace: s.logsink.GetNamespace().AsNamespace(), 210 infra: infra, 211 } 212 } 213 return nil 214 } 215 216 // LogDatagram creates a new build-level datagram log stream with the given name. 217 // Each call to WriteDatagram will produce a single datagram message in the 218 // stream. 219 // 220 // You must close the stream when you're done with it. 221 func (s *State) LogDatagram(name string, opts ...streamclient.Option) streamclient.DatagramWriter { 222 var ret streamclient.DatagramStream 223 224 if ls := s.logsink; ls != nil { 225 s.addLog(name, func(name string, relLdName ldTypes.StreamName) io.Closer { 226 var err error 227 ret, err = ls.NewDatagramStream(s.ctx, relLdName, opts...) 228 if err != nil { 229 panic(err) 230 } 231 return ret 232 }) 233 } 234 235 return ret 236 } 237 238 // Build returns a copy of the initial Build state. 239 // 240 // This is useful to access fields such as Infra, Tags, Ancestor ids etc. 241 // 242 // Changes to this copy will not reflect anywhere in the live Build state and 243 // not affect other calls to Build(). 244 // 245 // NOTE: It is recommended to use the PropertyModifier/PropertyReader functionality 246 // of this package to interact with Build Input Properties; They are encoded as 247 // Struct proto messages, which are extremely cumbersome to work with directly. 248 func (s *State) Build() *bbpb.Build { 249 if s.inputBuildPb == nil { 250 return nil 251 } 252 return proto.Clone(s.inputBuildPb).(*bbpb.Build) 253 } 254 255 // SynthesizeIOProto synthesizes a `.proto` file from the input and ouptut 256 // property messages declared at Start() time. 257 func (s *State) SynthesizeIOProto(o io.Writer) error { 258 _, err := iotools.WriteTracker(o, func(o io.Writer) error { 259 _ = func(format string, a ...any) { fmt.Fprintf(o, format, a...) } 260 // TODO(iannucci): implement 261 return nil 262 }) 263 return err 264 } 265 266 // private functions 267 268 type ctxState struct { 269 state *State 270 step *Step 271 } 272 273 // Returns the step name prefix including terminal "|". 274 func (c ctxState) stepNamePrefix() string { 275 if c.step == nil { 276 return "" 277 } 278 return c.step.name + "|" 279 } 280 281 var contextStateKey = "holds a ctxState" 282 283 func setState(ctx context.Context, state ctxState) context.Context { 284 return context.WithValue(ctx, &contextStateKey, state) 285 } 286 287 func getState(ctx context.Context) ctxState { 288 ret, _ := ctx.Value(&contextStateKey).(ctxState) 289 return ret 290 } 291 292 // Allows reads from buildPb and also must be held when sub-messages within 293 // buildPb are being written to. 294 // 295 // cb returns true if some portion of buildPB was mutated. 296 func (s *State) excludeCopy(cb func() bool) { 297 if s != nil { 298 s.buildPbMu.RLock() 299 defer s.buildPbMu.RUnlock() 300 301 if protoutil.IsEnded(s.buildPb.Output.Status) { 302 panic(errors.New("cannot mutate ended build")) 303 } 304 } 305 changed := cb() 306 if changed && s != nil && s.sendCh.C != nil { 307 s.sendCh.C <- s.buildPbVers.Add(1) 308 } 309 } 310 311 // cb returns true if some portion of buildPB was mutated. 312 // 313 // Allows writes to s.buildPb 314 func (s *State) mutate(cb func() bool) { 315 if s != nil { 316 s.buildPbMu.Lock() 317 defer s.buildPbMu.Unlock() 318 319 if protoutil.IsEnded(s.buildPb.Output.Status) { 320 panic(errors.New("cannot mutate ended build")) 321 } 322 } 323 changed := cb() 324 if changed && s != nil && s.sendCh.C != nil { 325 s.sendCh.C <- s.buildPbVers.Add(1) 326 } 327 } 328 329 func (s *State) registerStep(step *bbpb.Step) (passthrough *bbpb.Step, logNamespace, logSuffix string) { 330 passthrough = step 331 if s == nil { 332 return 333 } 334 335 s.mutate(func() bool { 336 step.Name = s.stepNames.resolveName(step.Name) 337 s.buildPb.Steps = append(s.buildPb.Steps, step) 338 logSuffix = fmt.Sprintf("step/%d", len(s.buildPb.Steps)-1) 339 340 return true 341 }) 342 logNamespace = string(s.logsink.GetNamespace()) 343 344 return 345 }