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  }