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

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