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 }