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

Unified Diff: luci_config/server/cfgclient/textproto/resolver.go

Issue 2578893002: server/config: Add text protobuf support. (Closed)
Patch Set: Rebase, comments. Created 3 years, 11 months 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 | luci_config/server/cfgclient/textproto/resolver_test.go » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: luci_config/server/cfgclient/textproto/resolver.go
diff --git a/luci_config/server/cfgclient/textproto/resolver.go b/luci_config/server/cfgclient/textproto/resolver.go
new file mode 100644
index 0000000000000000000000000000000000000000..f3b6e3eab312804c740c3fddfce8d58a7f710b77
--- /dev/null
+++ b/luci_config/server/cfgclient/textproto/resolver.go
@@ -0,0 +1,249 @@
+// 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.
+//
+// It uses the "luci-go" text protobuf multi-line extension. For more
+// information, see: github.com/luci/luci-go/common/proto
+//
+// The textproto protobuf Resolver internally formats its content as a binary
+// protobuf, rather than its raw text content. 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.
+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/luci_config/server/cfgclient"
+ "github.com/luci/luci-go/luci_config/server/cfgclient/backend"
+
+ "github.com/golang/protobuf/proto"
+)
+
+var typeOfProtoMessage = reflect.TypeOf((*proto.Message)(nil)).Elem()
+
+// BinaryFormat is the resolver's binary protobuf format string.
+const BinaryFormat = "github.com/luci/luci-go/server/config/TextProto:binary"
+
+// Message is a cfgclient.Resolver that resolves config data into proto.Message
+// instances by parsing the config data as a text protobuf.
+func Message(out proto.Message) interface {
+ cfgclient.Resolver
+ cfgclient.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 cfgclient.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 {
+ cfgclient.MultiResolver
+ cfgclient.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 cfgclient.MultiResolver.
+func (r *resolver) Resolve(it *backend.Item) error {
+ return r.resolveItem(r.single, it.Content, it.FormatSpec.Formatter)
+}
+
+// PrepareMulti implements cfgclient.MultiResolver.
+func (r *resolver) PrepareMulti(size int) {
+ slice := reflect.MakeSlice(r.multiDest.Type(), size, size)
+ r.multiDest.Set(slice)
+}
+
+// ResolveItemAt implements cfgclient.MultiResolver.
+func (r *resolver) ResolveItemAt(i int, it *backend.Item) error {
+ msgV := archetypeInstance(r.singleType)
+ if err := r.resolveItem(msgV.Interface().(proto.Message), it.Content, it.FormatSpec.Formatter); 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() backend.FormatSpec {
+ return backend.FormatSpec{BinaryFormat, r.messageName}
+}
+
+// RegisterFormatter registers the textproto Formatter with fr.
+func RegisterFormatter(fr *cfgclient.FormatterRegistry) {
+ fr.Register(BinaryFormat, &Formatter{})
+}
+
+// Formatter is a cfgclient.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 cfgclient.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 | luci_config/server/cfgclient/textproto/resolver_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698