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

Side by Side 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 unified diff | Download patch
« no previous file with comments | « no previous file | server/config/textproto/resolver_test.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2016 The LUCI Authors. All rights reserved.
2 // Use of this source code is governed under the Apache License, Version 2.0
3 // that can be found in the LICENSE file.
4
5 // 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.
6 //
7 // This has some advantages over raw text content caching:
8 // - More space-efficient.
9 // - Decodes faster.
10 // - If the config service protobuf schema differs from the application's
11 // compiled schema, and the schema change is responsible (adding, renamin g,
12 // repurposing) the binary cache value will continue to load where the te xt
13 // protobuf would fail.
14 //
15 // Additionally, it uses the "luci-go" text protobuf multi-line extension. For
16 // more information, see: github.com/luci/luci-go/common/proto
17 package textproto
18
19 import (
20 "bytes"
21 "reflect"
22 "strings"
23
24 "github.com/luci/luci-go/common/data/cmpbin"
25 "github.com/luci/luci-go/common/errors"
26 luciProto "github.com/luci/luci-go/common/proto"
27 "github.com/luci/luci-go/server/config"
28
29 "github.com/golang/protobuf/proto"
30 )
31
32 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.
33
34 // 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
35 const BinaryFormat = "github.com/luci/luci-go/server/config/TextProto:binary"
36
37 // Message is a config.Resolver that resolves config data into proto.Message
38 // instances by parsing the config data as a text protobuf.
39 func Message(out proto.Message) interface {
40 config.Resolver
41 config.FormattingResolver
42 } {
43 if out == nil {
44 panic("cannot pass a nil proto.Message instance")
45 }
46 return &resolver{
47 messageName: proto.MessageName(out),
48 single: out,
49 singleType: reflect.TypeOf(out),
50 }
51 }
52
53 // Slice is a config.MultiResolver which resolves a slice of configurations
54 // into a TextProto.
55 //
56 // out must be a pointer to a slice of some proto.Message implementation. If it
57 // isn't, this function will panic.
58 //
59 // For example:
60 //
61 // var out []*MyProtoMessages
62 // TextProtoSlice(&out)
63 func Slice(out interface{}) interface {
64 config.MultiResolver
65 config.FormattingResolver
66 } {
67 r := resolver{}
68
69 v := reflect.ValueOf(out)
70 if !r.loadMulti(v) {
71 panic(errors.Reason("%(type)s is not a pointer to a slice of pro tobuf message types").D("type", v.Type()).Err())
72 }
73 return &r
74 }
75
76 type resolver struct {
77 messageName string
78
79 single proto.Message
80 singleType reflect.Type
81
82 multiDest reflect.Value
83 }
84
85 func (r *resolver) loadMulti(v reflect.Value) bool {
86 t := v.Type()
87 if t.Kind() != reflect.Ptr {
88 return false
89 }
90 valElem := t.Elem()
91 if valElem.Kind() != reflect.Slice {
92 return false
93 }
94 sliceContent := valElem.Elem()
95 if sliceContent.Kind() != reflect.Ptr {
96 return false
97 }
98 if !sliceContent.Implements(typeOfProtoMessage) {
99 return false
100 }
101
102 r.messageName = proto.MessageName(archetypeMessage(sliceContent))
103 r.singleType = sliceContent
104 r.multiDest = v.Elem()
105 return true
106 }
107
108 // Resolve implements config.MultiResolver.
109 func (r *resolver) Resolve(it *config.Item) error {
110 return r.resolveItem(r.single, it.Content, it.Format)
111 }
112
113 // PrepareMulti implements config.MultiResolver.
114 func (r *resolver) PrepareMulti(size int) {
115 slice := reflect.MakeSlice(r.multiDest.Type(), size, size)
116 r.multiDest.Set(slice)
117 }
118
119 // ResolveItemAt implements config.MultiResolver.
120 func (r *resolver) ResolveItemAt(i int, it *config.Item) error {
121 msgV := archetypeInstance(r.singleType)
122 if err := r.resolveItem(msgV.Interface().(proto.Message), it.Content, it .Format); err != nil {
123 return err
124 }
125 r.multiDest.Index(i).Set(msgV)
126 return nil
127 }
128
129 func (r *resolver) resolveItem(out proto.Message, content string, format string) error {
130 switch format {
131 case "":
132 // Not formatted (text protobuf).
133 if err := luciProto.UnmarshalTextML(content, out); err != nil {
134 return errors.Annotate(err).Reason("failed to unmarshal text protobuf").Err()
135 }
136 return nil
137
138 case BinaryFormat:
139 if err := parseBinaryContent(content, out); err != nil {
140 return errors.Annotate(err).Reason("failed to unmarshal binary protobuf").Err()
141 }
142 return nil
143
144 default:
145 return errors.Reason("unsupported content format: %(format)q").D ("format", format).Err()
146 }
147 }
148
149 func (r *resolver) Format() (string, string) { return BinaryFormat, r.messageNam e }
150
151 // RegisterFormatter registers the textproto Formatter with fr.
152 func RegisterFormatter(fr *config.FormatterRegistry) {
153 fr.Register(BinaryFormat, &Formatter{})
154 }
155
156 // Formatter is a config.Formatter implementation bound to a specific
157 // protobuf message.
158 //
159 // It takes a text protobuf representation of that message as input and returns
160 // a binary protobuf representation as output.
161 type Formatter struct{}
162
163 // FormatItem implements config.Formatter.
164 func (f *Formatter) FormatItem(c, fd string) (string, error) {
165 archetype := proto.MessageType(fd)
166 if archetype == nil {
167 return "", errors.Reason("unknown proto.Message type %(type)q in formatter data").
168 D("type", fd).Err()
169 }
170 msg := archetypeMessage(archetype)
171
172 // Convert from config to protobuf.
173 if err := luciProto.UnmarshalTextML(c, msg); err != nil {
174 return "", errors.Annotate(err).Reason("failed to unmarshal text protobuf content").Err()
175 }
176
177 // Binary format.
178 bc, err := makeBinaryContent(msg)
179 if err != nil {
180 return "", errors.Annotate(err).Err()
181 }
182 return bc, nil
183 }
184
185 // t is a pointer to a proto.Message instance.
186 func archetypeInstance(t reflect.Type) reflect.Value {
187 return reflect.New(t.Elem())
188 }
189
190 func archetypeMessage(t reflect.Type) proto.Message {
191 return archetypeInstance(t).Interface().(proto.Message)
192 }
193
194 // makeBinaryContent constructs a binary content string from text protobuf
195 // content.
196 //
197 // The binary content is formatted by concatenating two "cmpbin" binary values
198 // together:
199 // [Message Name] | [Marshaled Message Data]
200 func makeBinaryContent(msg proto.Message) (string, error) {
201 d, err := proto.Marshal(msg)
202 if err != nil {
203 return "", errors.Annotate(err).Reason("failed to marshal messag e").Err()
204 }
205
206 var buf bytes.Buffer
207 if _, err := cmpbin.WriteString(&buf, proto.MessageName(msg)); err != ni l {
208 return "", errors.Annotate(err).Reason("failed to write message name").Err()
209 }
210 if _, err := cmpbin.WriteBytes(&buf, d); err != nil {
211 return "", errors.Annotate(err).Reason("failed to write binary m essage content").Err()
212 }
213 return buf.String(), nil
214 }
215
216 // parseBinaryContent parses a binary content string, pulling out the message
217 // type and marshalled message data. It then unmarshals the specified type into
218 // a new message based on the archetype.
219 //
220 // If the binary content's declared type doesn't match the archetype, or if the
221 // binary content is invalid, an error will be returned.
222 func parseBinaryContent(v string, msg proto.Message) error {
223 r := strings.NewReader(v)
224 encName, _, err := cmpbin.ReadString(r)
225 if err != nil {
226 return errors.Annotate(err).Reason("failed to read message name" ).Err()
227 }
228
229 // Construct a message for this.
230 if name := proto.MessageName(msg); name != encName {
231 return errors.Reason("message name %(name)q doesn't match encode d name %(enc)q").
232 D("name", name).D("enc", encName).Err()
233 }
234
235 // We have the right message, unmarshal.
236 d, _, err := cmpbin.ReadBytes(r)
237 if err != nil {
238 return errors.Annotate(err).Reason("failed to read binary messag e content").Err()
239 }
240 if err := proto.Unmarshal(d, msg); err != nil {
241 return errors.Annotate(err).Reason("failed to unmarshal message" ).Err()
242 }
243 return nil
244 }
OLDNEW
« 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