Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(422)

Unified Diff: server/config/textproto/resolver.go

Issue 2578893002: server/config: Add text protobuf support. (Closed)
Patch Set: Update MultiResolver interface. Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | server/config/textproto/resolver_test.go » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: server/config/textproto/resolver.go
diff --git a/server/config/textproto/resolver.go b/server/config/textproto/resolver.go
new file mode 100644
index 0000000000000000000000000000000000000000..ea71cbf4f30f453585ef0d972635b1bb67a4a347
--- /dev/null
+++ b/server/config/textproto/resolver.go
@@ -0,0 +1,244 @@
+// Copyright 2016 The LUCI Authors. All rights reserved.
+// Use of this source code is governed under the Apache License, Version 2.0
+// that can be found in the LICENSE file.
+
+// Package textproto implements a textproto config service Resolver.
iannucci 2017/01/07 20:22:11 I would mention that it actually stores the binary
dnj 2017/01/10 03:26:37 Done.
+//
+// This has some advantages over raw text content caching:
+// - More space-efficient.
+// - Decodes faster.
+// - If the config service protobuf schema differs from the application's
+// compiled schema, and the schema change is responsible (adding, renaming,
+// repurposing) the binary cache value will continue to load where the text
+// protobuf would fail.
+//
+// Additionally, it uses the "luci-go" text protobuf multi-line extension. For
+// more information, see: github.com/luci/luci-go/common/proto
+package textproto
+
+import (
+ "bytes"
+ "reflect"
+ "strings"
+
+ "github.com/luci/luci-go/common/data/cmpbin"
+ "github.com/luci/luci-go/common/errors"
+ luciProto "github.com/luci/luci-go/common/proto"
+ "github.com/luci/luci-go/server/config"
+
+ "github.com/golang/protobuf/proto"
+)
+
+var typeOfProtoMessage = reflect.TypeOf((*proto.Message)(nil)).Elem()
iannucci 2017/01/07 20:22:11 really dumb that the proto package doesn't have th
dnj 2017/01/10 03:26:37 Acknowledged.
+
+// BinaryFormat is the resolver's binary protobuf format string.
iannucci 2017/01/07 20:22:11 I would make sure that Format is documented so tha
dnj 2017/01/10 03:26:37 The "subformat" part is variable, dependent on the
+const BinaryFormat = "github.com/luci/luci-go/server/config/TextProto:binary"
+
+// Message is a config.Resolver that resolves config data into proto.Message
+// instances by parsing the config data as a text protobuf.
+func Message(out proto.Message) interface {
+ config.Resolver
+ config.FormattingResolver
+} {
+ if out == nil {
+ panic("cannot pass a nil proto.Message instance")
+ }
+ return &resolver{
+ messageName: proto.MessageName(out),
+ single: out,
+ singleType: reflect.TypeOf(out),
+ }
+}
+
+// Slice is a config.MultiResolver which resolves a slice of configurations
+// into a TextProto.
+//
+// out must be a pointer to a slice of some proto.Message implementation. If it
+// isn't, this function will panic.
+//
+// For example:
+//
+// var out []*MyProtoMessages
+// TextProtoSlice(&out)
+func Slice(out interface{}) interface {
+ config.MultiResolver
+ config.FormattingResolver
+} {
+ r := resolver{}
+
+ v := reflect.ValueOf(out)
+ if !r.loadMulti(v) {
+ panic(errors.Reason("%(type)s is not a pointer to a slice of protobuf message types").D("type", v.Type()).Err())
+ }
+ return &r
+}
+
+type resolver struct {
+ messageName string
+
+ single proto.Message
+ singleType reflect.Type
+
+ multiDest reflect.Value
+}
+
+func (r *resolver) loadMulti(v reflect.Value) bool {
+ t := v.Type()
+ if t.Kind() != reflect.Ptr {
+ return false
+ }
+ valElem := t.Elem()
+ if valElem.Kind() != reflect.Slice {
+ return false
+ }
+ sliceContent := valElem.Elem()
+ if sliceContent.Kind() != reflect.Ptr {
+ return false
+ }
+ if !sliceContent.Implements(typeOfProtoMessage) {
+ return false
+ }
+
+ r.messageName = proto.MessageName(archetypeMessage(sliceContent))
+ r.singleType = sliceContent
+ r.multiDest = v.Elem()
+ return true
+}
+
+// Resolve implements config.MultiResolver.
+func (r *resolver) Resolve(it *config.Item) error {
+ return r.resolveItem(r.single, it.Content, it.Format)
+}
+
+// PrepareMulti implements config.MultiResolver.
+func (r *resolver) PrepareMulti(size int) {
+ slice := reflect.MakeSlice(r.multiDest.Type(), size, size)
+ r.multiDest.Set(slice)
+}
+
+// ResolveItemAt implements config.MultiResolver.
+func (r *resolver) ResolveItemAt(i int, it *config.Item) error {
+ msgV := archetypeInstance(r.singleType)
+ if err := r.resolveItem(msgV.Interface().(proto.Message), it.Content, it.Format); err != nil {
+ return err
+ }
+ r.multiDest.Index(i).Set(msgV)
+ return nil
+}
+
+func (r *resolver) resolveItem(out proto.Message, content string, format string) error {
+ switch format {
+ case "":
+ // Not formatted (text protobuf).
+ if err := luciProto.UnmarshalTextML(content, out); err != nil {
+ return errors.Annotate(err).Reason("failed to unmarshal text protobuf").Err()
+ }
+ return nil
+
+ case BinaryFormat:
+ if err := parseBinaryContent(content, out); err != nil {
+ return errors.Annotate(err).Reason("failed to unmarshal binary protobuf").Err()
+ }
+ return nil
+
+ default:
+ return errors.Reason("unsupported content format: %(format)q").D("format", format).Err()
+ }
+}
+
+func (r *resolver) Format() (string, string) { return BinaryFormat, r.messageName }
+
+// RegisterFormatter registers the textproto Formatter with fr.
+func RegisterFormatter(fr *config.FormatterRegistry) {
+ fr.Register(BinaryFormat, &Formatter{})
+}
+
+// Formatter is a config.Formatter implementation bound to a specific
+// protobuf message.
+//
+// It takes a text protobuf representation of that message as input and returns
+// a binary protobuf representation as output.
+type Formatter struct{}
+
+// FormatItem implements config.Formatter.
+func (f *Formatter) FormatItem(c, fd string) (string, error) {
+ archetype := proto.MessageType(fd)
+ if archetype == nil {
+ return "", errors.Reason("unknown proto.Message type %(type)q in formatter data").
+ D("type", fd).Err()
+ }
+ msg := archetypeMessage(archetype)
+
+ // Convert from config to protobuf.
+ if err := luciProto.UnmarshalTextML(c, msg); err != nil {
+ return "", errors.Annotate(err).Reason("failed to unmarshal text protobuf content").Err()
+ }
+
+ // Binary format.
+ bc, err := makeBinaryContent(msg)
+ if err != nil {
+ return "", errors.Annotate(err).Err()
+ }
+ return bc, nil
+}
+
+// t is a pointer to a proto.Message instance.
+func archetypeInstance(t reflect.Type) reflect.Value {
+ return reflect.New(t.Elem())
+}
+
+func archetypeMessage(t reflect.Type) proto.Message {
+ return archetypeInstance(t).Interface().(proto.Message)
+}
+
+// makeBinaryContent constructs a binary content string from text protobuf
+// content.
+//
+// The binary content is formatted by concatenating two "cmpbin" binary values
+// together:
+// [Message Name] | [Marshaled Message Data]
+func makeBinaryContent(msg proto.Message) (string, error) {
+ d, err := proto.Marshal(msg)
+ if err != nil {
+ return "", errors.Annotate(err).Reason("failed to marshal message").Err()
+ }
+
+ var buf bytes.Buffer
+ if _, err := cmpbin.WriteString(&buf, proto.MessageName(msg)); err != nil {
+ return "", errors.Annotate(err).Reason("failed to write message name").Err()
+ }
+ if _, err := cmpbin.WriteBytes(&buf, d); err != nil {
+ return "", errors.Annotate(err).Reason("failed to write binary message content").Err()
+ }
+ return buf.String(), nil
+}
+
+// parseBinaryContent parses a binary content string, pulling out the message
+// type and marshalled message data. It then unmarshals the specified type into
+// a new message based on the archetype.
+//
+// If the binary content's declared type doesn't match the archetype, or if the
+// binary content is invalid, an error will be returned.
+func parseBinaryContent(v string, msg proto.Message) error {
+ r := strings.NewReader(v)
+ encName, _, err := cmpbin.ReadString(r)
+ if err != nil {
+ return errors.Annotate(err).Reason("failed to read message name").Err()
+ }
+
+ // Construct a message for this.
+ if name := proto.MessageName(msg); name != encName {
+ return errors.Reason("message name %(name)q doesn't match encoded name %(enc)q").
+ D("name", name).D("enc", encName).Err()
+ }
+
+ // We have the right message, unmarshal.
+ d, _, err := cmpbin.ReadBytes(r)
+ if err != nil {
+ return errors.Annotate(err).Reason("failed to read binary message content").Err()
+ }
+ if err := proto.Unmarshal(d, msg); err != nil {
+ return errors.Annotate(err).Reason("failed to unmarshal message").Err()
+ }
+ return nil
+}
« no previous file with comments | « no previous file | server/config/textproto/resolver_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698