blob: 41a77f69ff7334cc5aa1758df4215cfff1f0fb52 [file] [log] [blame]
// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package build
import (
"context"
"reflect"
"sync"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/structpb"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
)
type outputPropertyReservations struct {
locs resLocations
}
func (o *outputPropertyReservations) reserve(ns string, skip int) {
o.locs.reserve(ns, "PropertyModifier", skip+1)
}
func (o *outputPropertyReservations) clear() {
o.locs.clear(nil)
}
var propModifierReservations = outputPropertyReservations{}
// MakePropertyModifier allows your library/module to reserve a section of the
// output properties for itself.
//
// You can use this to obtain a write function (replace contents at namespace)
// and/or a merge function (do proto.Merge on the current contents of that
// namespace). If one of the function pointers is nil, it will be skipped (at
// least one must be non-nil). If both function pointers are provided, their
// types must exactly agree.
//
// Attempting to reserve duplicate namespaces will panic. The namespace refers
// to the top-level property key. It is recommended that:
// * The `ns` begins with '$'.
// * The value after the '$' is the canonical Go package name for your
// library.
//
// You should call this at init()-time like:
//
// var propWriter func(context.Context, *MyMessage)
// var propMerger func(context.Context, *MyMessage)
//
// func init() {
// // one of the two function pointers may be nil
// MakePropertyModifier("$some/namespace", &propWriter, &propMerger)
// }
//
// Note that all MakePropertyModifier invocations must happen BEFORE the build
// is Started. Otherwise invoking the returned writer/merger functions will
// panic.
//
// In Go2 this will be less weird:
// type PropertyModifier[T proto.Message] interface {
// Write(context.Context, value T) // assigns 'value'
// Merge(context.Context, value T) // does proto.Merge(current, value)
// }
// func MakePropertyModifier[T proto.Message](ns string) PropertyModifier[T]
func MakePropertyModifier(ns string, writeFnptr, mergeFnptr interface{}) {
propModifierReservations.reserve(ns, 1)
writer, merger, _ := getWriteMergerFnValues(true, writeFnptr, mergeFnptr)
impl := func(args []reflect.Value, op string, opFn func(*outputPropertyState, proto.Message)) []reflect.Value {
if args[1].IsNil() {
return nil
}
ctx := args[0].Interface().(context.Context)
cstate := getState(ctx)
msg := args[1].Interface().(proto.Message)
if st := cstate.state; st != nil {
st.excludeCopy(func() bool {
if prop := st.outputProperties[ns]; prop != nil {
opFn(prop, msg)
return true
}
panic(errors.Reason(
"MakePropertyModifier[%s] for namespace %q was created after the current build started: %s",
op, ns, propModifierReservations.locs.get(ns)).Err())
})
} else {
// noop mode, log incoming property
val, err := protojson.Marshal(msg)
if err != nil {
panic(err)
}
logging.Infof(ctx, "%s output property %q: %q", op, ns, string(val))
}
return nil
}
if writer.Kind() == reflect.Func {
writer.Set(reflect.MakeFunc(writer.Type(), func(args []reflect.Value) []reflect.Value {
return impl(args, "writing", (*outputPropertyState).set)
}))
}
if merger.Kind() == reflect.Func {
merger.Set(reflect.MakeFunc(merger.Type(), func(args []reflect.Value) []reflect.Value {
return impl(args, "merging", (*outputPropertyState).merge)
}))
}
}
func getWriteMergerFnValues(withContext bool, writeFnptr, mergeFnptr interface{}) (writer, merger reflect.Value, msgT protoreflect.Message) {
if writeFnptr == nil && mergeFnptr == nil {
panic("at least one of {writeFnptr, mergeFnptr} must be non-nil")
}
var msg error
var typeSig []reflect.Type
if withContext {
msg = errors.New("fnptr is not `func[T proto.Message](context.Context, T)`")
typeSig = []reflect.Type{ctxType, protoMessageType}
} else {
msg = errors.New("fnptr is not `func[T proto.Message](T)`")
typeSig = []reflect.Type{protoMessageType}
}
// We assign msgT in both cases in case one of writeFnptr or mergeFnptr is
// nil; We check that they are the same type when we assert that writer and
// merger have the same types.
if writeFnptr != nil {
writer, msgT = derefFnPtr(msg, writeFnptr, typeSig, nil)
}
if mergeFnptr != nil {
merger, msgT = derefFnPtr(msg, mergeFnptr, typeSig, nil)
}
if writeFnptr != nil && mergeFnptr != nil {
if reflect.TypeOf(writeFnptr) != reflect.TypeOf(mergeFnptr) {
panic("{writeFnptr, mergeFnptr} types do not match")
}
}
return
}
type outputPropertyState struct {
mu sync.Mutex
// The current state of this output property.
msg proto.Message
// cached is non-nil when it has an up-to-date serialization of `msg`.
cached *structpb.Struct
}
func msgIsEmpty(msg proto.Message) bool {
// see if st.msg is nil, or if it's empty; In either case we return a nil *Struct.
if msg == nil {
return true
}
isEmpty := true
msg.ProtoReflect().Range(func(protoreflect.FieldDescriptor, protoreflect.Value) bool {
isEmpty = false
return false // exit on the first callback
})
return isEmpty
}
func (st *outputPropertyState) getStructClone() *structpb.Struct {
if st == nil {
return nil
}
st.mu.Lock()
defer st.mu.Unlock()
// see if st.msg is nil, or if it's empty; In either case we return a nil *Struct.
if msgIsEmpty(st.msg) {
return nil
}
if st.cached == nil {
json, err := protojson.Marshal(st.msg)
if err != nil {
panic(errors.Annotate(err, "marshaling output property").Err())
}
st.cached, _ = structpb.NewStruct(nil)
if err := protojson.Unmarshal(json, st.cached); err != nil {
panic(errors.Annotate(err, "unmarshaling output property").Err())
}
}
return proto.Clone(st.cached).(*structpb.Struct)
}
func (st *outputPropertyState) set(msg proto.Message) {
st.mu.Lock()
defer st.mu.Unlock()
st.cached = nil
st.msg = proto.Clone(msg)
}
func (st *outputPropertyState) merge(msg proto.Message) {
st.mu.Lock()
defer st.mu.Unlock()
st.cached = nil
if msgIsEmpty(st.msg) {
st.msg = proto.Clone(msg)
} else {
proto.Merge(st.msg, msg)
}
}