go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/build/output_properties.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  	"reflect"
    20  	"sync"
    21  
    22  	"google.golang.org/protobuf/encoding/protojson"
    23  	"google.golang.org/protobuf/proto"
    24  	"google.golang.org/protobuf/reflect/protoreflect"
    25  	"google.golang.org/protobuf/types/known/structpb"
    26  
    27  	"go.chromium.org/luci/common/errors"
    28  	"go.chromium.org/luci/common/logging"
    29  )
    30  
    31  type outputPropertyReservations struct {
    32  	locs resLocations
    33  }
    34  
    35  func (o *outputPropertyReservations) reserve(ns string, skip int) {
    36  	o.locs.reserve(ns, "PropertyModifier", skip+1)
    37  }
    38  
    39  func (o *outputPropertyReservations) clear() {
    40  	o.locs.clear(nil)
    41  }
    42  
    43  var propModifierReservations = outputPropertyReservations{}
    44  
    45  // MakePropertyModifier allows your library/module to reserve a section of the
    46  // output properties for itself.
    47  //
    48  // You can use this to obtain a write function (replace contents at namespace)
    49  // and/or a merge function (do proto.Merge on the current contents of that
    50  // namespace). If one of the function pointers is nil, it will be skipped (at
    51  // least one must be non-nil). If both function pointers are provided, their
    52  // types must exactly agree.
    53  //
    54  // Attempting to reserve duplicate namespaces will panic. The namespace refers
    55  // to the top-level property key. It is recommended that:
    56  //   - The `ns` begins with '$'.
    57  //   - The value after the '$' is the canonical Go package name for your
    58  //     library.
    59  //
    60  // You should call this at init()-time like:
    61  //
    62  //	var propWriter func(context.Context, *MyMessage)
    63  //	var propMerger func(context.Context, *MyMessage)
    64  //
    65  //	func init() {
    66  //	  // one of the two function pointers may be nil
    67  //	  MakePropertyModifier("$some/namespace", &propWriter, &propMerger)
    68  //	}
    69  //
    70  // Note that all MakePropertyModifier invocations must happen BEFORE the build
    71  // is Started. Otherwise invoking the returned writer/merger functions will
    72  // panic.
    73  //
    74  // In Go2 this will be less weird:
    75  //
    76  //	type PropertyModifier[T proto.Message] interface {
    77  //	  Write(context.Context, value T) // assigns 'value'
    78  //	  Merge(context.Context, value T) // does proto.Merge(current, value)
    79  //	}
    80  //	func MakePropertyModifier[T proto.Message](ns string) PropertyModifier[T]
    81  func MakePropertyModifier(ns string, writeFnptr, mergeFnptr any) {
    82  	propModifierReservations.reserve(ns, 1)
    83  	writer, merger, _ := getWriteMergerFnValues(true, writeFnptr, mergeFnptr)
    84  
    85  	impl := func(args []reflect.Value, op string, opFn func(*outputPropertyState, proto.Message)) []reflect.Value {
    86  		if args[1].IsNil() {
    87  			return nil
    88  		}
    89  
    90  		ctx := args[0].Interface().(context.Context)
    91  		cstate := getState(ctx)
    92  		msg := args[1].Interface().(proto.Message)
    93  
    94  		if st := cstate.state; st != nil {
    95  			st.excludeCopy(func() bool {
    96  				if prop := st.outputProperties[ns]; prop != nil {
    97  					opFn(prop, msg)
    98  					return true
    99  				}
   100  
   101  				panic(errors.Reason(
   102  					"MakePropertyModifier[%s] for namespace %q was created after the current build started: %s",
   103  					op, ns, propModifierReservations.locs.get(ns)).Err())
   104  			})
   105  		} else {
   106  			// noop mode, log incoming property
   107  			val, err := protojson.Marshal(msg)
   108  			if err != nil {
   109  				panic(err)
   110  			}
   111  			logging.Infof(ctx, "%s output property %q: %q", op, ns, string(val))
   112  		}
   113  		return nil
   114  	}
   115  
   116  	if writer.Kind() == reflect.Func {
   117  		writer.Set(reflect.MakeFunc(writer.Type(), func(args []reflect.Value) []reflect.Value {
   118  			return impl(args, "writing", (*outputPropertyState).set)
   119  		}))
   120  	}
   121  
   122  	if merger.Kind() == reflect.Func {
   123  		merger.Set(reflect.MakeFunc(merger.Type(), func(args []reflect.Value) []reflect.Value {
   124  			return impl(args, "merging", (*outputPropertyState).merge)
   125  		}))
   126  	}
   127  }
   128  
   129  func getWriteMergerFnValues(withContext bool, writeFnptr, mergeFnptr any) (writer, merger reflect.Value, msgT protoreflect.Message) {
   130  	if writeFnptr == nil && mergeFnptr == nil {
   131  		panic("at least one of {writeFnptr, mergeFnptr} must be non-nil")
   132  	}
   133  
   134  	var msg error
   135  	var typeSig []reflect.Type
   136  	if withContext {
   137  		msg = errors.New("fnptr is not `func[T proto.Message](context.Context, T)`")
   138  		typeSig = []reflect.Type{ctxType, protoMessageType}
   139  	} else {
   140  		msg = errors.New("fnptr is not `func[T proto.Message](T)`")
   141  		typeSig = []reflect.Type{protoMessageType}
   142  	}
   143  
   144  	// We assign msgT in both cases in case one of writeFnptr or mergeFnptr is
   145  	// nil; We check that they are the same type when we assert that writer and
   146  	// merger have the same types.
   147  	if writeFnptr != nil {
   148  		writer, msgT = derefFnPtr(msg, writeFnptr, typeSig, nil)
   149  	}
   150  	if mergeFnptr != nil {
   151  		merger, msgT = derefFnPtr(msg, mergeFnptr, typeSig, nil)
   152  	}
   153  
   154  	if writeFnptr != nil && mergeFnptr != nil {
   155  		if reflect.TypeOf(writeFnptr) != reflect.TypeOf(mergeFnptr) {
   156  			panic("{writeFnptr, mergeFnptr} types do not match")
   157  		}
   158  	}
   159  
   160  	return
   161  }
   162  
   163  type outputPropertyState struct {
   164  	mu sync.Mutex
   165  
   166  	// The current state of this output property.
   167  	msg proto.Message
   168  
   169  	// cached is non-nil when it has an up-to-date serialization of `msg`.
   170  	cached *structpb.Struct
   171  }
   172  
   173  func msgIsEmpty(msg proto.Message) bool {
   174  	// see if st.msg is nil, or if it's empty; In either case we return a nil *Struct.
   175  	if msg == nil {
   176  		return true
   177  	}
   178  	isEmpty := true
   179  	msg.ProtoReflect().Range(func(protoreflect.FieldDescriptor, protoreflect.Value) bool {
   180  		isEmpty = false
   181  		return false // exit on the first callback
   182  	})
   183  	return isEmpty
   184  }
   185  
   186  func (st *outputPropertyState) getStructClone() *structpb.Struct {
   187  	if st == nil {
   188  		return nil
   189  	}
   190  
   191  	st.mu.Lock()
   192  	defer st.mu.Unlock()
   193  
   194  	// see if st.msg is nil, or if it's empty; In either case we return a nil *Struct.
   195  	if msgIsEmpty(st.msg) {
   196  		return nil
   197  	}
   198  
   199  	if st.cached == nil {
   200  		json, err := protojson.Marshal(st.msg)
   201  		if err != nil {
   202  			panic(errors.Annotate(err, "marshaling output property").Err())
   203  		}
   204  		st.cached, _ = structpb.NewStruct(nil)
   205  		if err := protojson.Unmarshal(json, st.cached); err != nil {
   206  			panic(errors.Annotate(err, "unmarshaling output property").Err())
   207  		}
   208  	}
   209  
   210  	return proto.Clone(st.cached).(*structpb.Struct)
   211  }
   212  
   213  func (st *outputPropertyState) set(msg proto.Message) {
   214  	st.mu.Lock()
   215  	defer st.mu.Unlock()
   216  
   217  	st.cached = nil
   218  	st.msg = proto.Clone(msg)
   219  }
   220  
   221  func (st *outputPropertyState) merge(msg proto.Message) {
   222  	st.mu.Lock()
   223  	defer st.mu.Unlock()
   224  
   225  	st.cached = nil
   226  	if msgIsEmpty(st.msg) {
   227  		st.msg = proto.Clone(msg)
   228  	} else {
   229  		proto.Merge(st.msg, msg)
   230  	}
   231  }