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

Side by Side Diff: go/src/infra/libs/epclient/epclient.go

Issue 1153473008: A client/server helper wrapper for endpoints in Go. (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: rename to better package Created 5 years, 6 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
OLDNEW
(Empty)
1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 // Package epclient provides a simple implementation of a Google Cloud Endpoints
6 // client (for services implemented in Go).
7 //
8 // Unlike a traditional endpoitns client, the interfaces and datatypes are
9 // statically built with the application, using the "infra/gae/libs/endpoints"
10 // library.
11 //
12 // Example usage:
13 //
14 // import "github.com/GoogleCloudPlatform/go-endpoints/endpoints"
15 // import infra_endpoints "infra/gae/libs/endpoints"
Vadim Sh. 2015/06/03 18:13:39 I'm sort of against intermixing GAE and non-GAE li
16 // import "some/service"
17 //
18 // // service.Info = *endpoints.ServiceInfo{...}
19 // // service.MethodInfo = *infra_endpoints.MethodInfoMap
20 //
21 // // service.
22 //
23 // func
24 //
25 package epclient
26
27 import (
28 "bytes"
29 "encoding/json"
30 "fmt"
31 "golang.org/x/net/context"
32 "io"
33 "io/ioutil"
34 "net"
35 "net/http"
36 "reflect"
37 "regexp"
38 "strings"
39 "sync"
40 "text/template"
41 "time"
42
43 "github.com/GoogleCloudPlatform/go-endpoints/endpoints"
44 "github.com/luci/luci-go/common/logging"
Vadim Sh. 2015/06/03 18:13:39 nit: seems weird to group luci-go import for go-en
45
46 "github.com/luci/luci-go/common/errors"
47 )
48
49 var backoffSlot = 50 * time.Millisecond
50
51 // Backoff calculates an amount of time to back off, depending on the number
52 // of failures which have occured so far. It is non-randomized to aid in
53 // determinism during tests.
54 func Backoff(failures uint) time.Duration {
55 const failcap = 10
56
57 if failures > failcap {
58 failures = failcap
59 }
60 return backoffSlot * time.Duration(1<<failures)
61 }
62
63 // Client is an endpoints-client which provides a very simple in/out RPC
64 // interface for a given endpoints server.
65 //
66 // Mapping is done statically using the infra/gae/libs/endpoints.MethodInfoMap
67 // of a given service. This means that you need to recompile the client any time
68 // you change the interface of your service (but honestly, you probably needed
69 // to do that anyway). Serialization and deserialization follow the
70 // encoding/json rules. The service implementation should expose the correct
71 // input and output types (along with any json field tags) necessary to interact
72 // with the service.
73 type Client interface {
74 // DoWithRetries will try the given method up to `upto` times before ret urning
75 // an error. An `upto` value of 0 will not do the API call at all.
76 //
77 // in and out must be the approprite concrete Go types which correspond to the
78 // method.
79 //
80 // methodName is the service method name, not the REST name for the api (e.g.
81 // it would be the Method portion of `Service.Method`, if you were hitti ng
82 // the spi interface).
83 //
84 // DoWithRetries will calculate a time to back off between attempts usin g
85 // the Backoff method.
86 DoWithRetries(serviceName, methodName string, in interface{}, out interf ace{}, upto uint) error
Vadim Sh. 2015/06/03 18:13:39 consider using https://github.com/luci/luci-go/tre
87
88 ForService(serviceName string) (ServiceClient, error)
Vadim Sh. 2015/06/03 18:13:39 document
89 }
90
91 // ServiceClient is like a Client, but restricted to a single service under a
92 // given endpoints server.
93 type ServiceClient interface {
94 DoWithRetries(methodName string, in interface{}, out interface{}, upto u int) error
95 }
96
97 var mark = errors.MakeMarkFn("endpoints")
98
99 type client struct {
100 sync.Mutex
101
102 url string
103
104 ctx context.Context
105 serverDef *endpoints.Server
106 templateMap map[string]map[string]*templateEntry
107
108 // for testing. Returns equivalent of http.Do and a closer function whic h
109 // should be defer'd to close the underlying client.
110 mkHTTPDoer func() (
111 doer func(*http.Request) (*http.Response, error),
112 closer func())
113 }
114
115 var _ Client = (*client)(nil)
Vadim Sh. 2015/06/03 18:13:39 why? NewClient does this type check already.
116
117 // NewClient returns a new Client object bound to hit `url`, which should be
118 // the url to the service (e.g. "https://foo.appspot.com"). It will expect
119 // to find the REST-style APIs at "<url>/_ah/api/<service>/<version>/<api>".
120 func NewClient(ctx context.Context, url string, s *endpoints.Server) Client {
121 // TODO(riannucci): provide an 'spi' mode.
122 return &client{
123 url: url,
124 ctx: ctx,
125 serverDef: s,
126 templateMap: map[string]map[string]*templateEntry{},
127 mkHTTPDoer: mkHTTPDoer,
128 }
129 }
130
131 var getFieldsRE = regexp.MustCompile("{{\\.([^}]*)}}")
132
133 type templateEntry struct {
134 *template.Template
135 used map[string]struct{}
136 si *endpoints.ServiceInfo
137 mi *endpoints.MethodInfo
138
139 reqType reflect.Type
140 rspType reflect.Type
141 }
142
143 func (c *client) getTemplateMap(serviceName string) map[string]*templateEntry {
144 c.Lock()
145 if m, ok := c.templateMap[serviceName]; ok {
146 c.Unlock()
147 return m
148 }
149 c.Unlock()
150
151 templateConvert := strings.NewReplacer("{", "{{.", "}", "}}")
152 service := c.serverDef.ServiceByName(serviceName)
153 if service == nil {
154 c.Lock()
155 defer c.Unlock()
156 c.templateMap[serviceName] = nil
157 return nil
158 }
159
160 methods := service.Methods()
161 ret := make(map[string]*templateEntry, len(methods))
162
163 // due to a bug (feature?) in go-endpoints, method.Info().Name is always
164 // lowercase, even when MethodByName expects the uppercase name.
165 // So let's reflect and grab those keys :)
166 methodNames := reflect.ValueOf(service).Elem().FieldByName("methods").Ma pKeys()
167 for _, nameV := range methodNames {
168 name := nameV.String()
169 method := service.MethodByName(name)
170 info := method.Info()
171 path := templateConvert.Replace(info.Path)
172
173 usedFields := map[string]struct{}{}
174 for _, submatches := range getFieldsRE.FindAllStringSubmatch(pat h, -1) {
175 usedFields[submatches[1]] = struct{}{}
176 }
177
178 ret[name] = &templateEntry{
179 template.Must(template.New(name).Parse(path)),
180 usedFields,
181 service.Info(),
182 info,
183 reflect.PtrTo(method.ReqType),
184 reflect.PtrTo(method.RespType),
185 }
186 }
187
188 c.Lock()
189 defer c.Unlock()
190 c.templateMap[serviceName] = ret // who cares if we overwrite it?
191 return ret
192 }
193
194 func (c *client) getMethodInfo(serviceName, methodName string) (*templateEntry, error) {
195 m := c.getTemplateMap(serviceName)
196 if m == nil {
197 return nil, mark(fmt.Errorf("getMethodInfo: no such service %s", serviceName))
198 }
199
200 methodTmpl := m[methodName]
201 if methodTmpl == nil {
202 return nil, mark(fmt.Errorf("getMethodInfo: no such method %s.%s ", serviceName, methodName))
203 }
204
205 return methodTmpl, nil
206 }
207
208 func renderURL(base string, t *templateEntry, requestData interface{}) (string, error) {
209 buf := &bytes.Buffer{}
210 if err := t.Execute(buf, requestData); err != nil {
211 return "", err
212 }
213
214 ret := fmt.Sprintf("%s/_ah/api/%s/%s/%s", base,
215 t.si.Name, t.si.Version, buf.String())
216
217 addedQuestion := false
218
219 if t.mi.HTTPMethod == "GET" && requestData != nil {
220 val := reflect.ValueOf(requestData)
221 stype := reflect.TypeOf(requestData)
222 if stype.Kind() == reflect.Ptr {
223 stype = stype.Elem()
224 val = val.Elem()
225 }
226 for i := 0; i < stype.NumField(); i++ {
227 f := stype.Field(i)
228 if _, skip := t.used[f.Name]; !skip {
229 connect := "?"
230 if addedQuestion {
231 connect = "&"
232 }
233 addedQuestion = true
234 marshalled, err := json.Marshal(val.Field(i).Int erface())
235 if err != nil {
236 return "", err
237 }
238 ret = fmt.Sprintf("%s%s%s=%s", ret, connect, f.N ame, string(marshalled))
239 }
240 }
241 }
242
243 return ret, nil
244 }
245
246 func (c *client) DoWithRetries(serviceName, methodName string, in interface{}, o ut interface{}, upto uint) (err error) {
247 for i := uint(0); i < upto; i++ {
248 err = c.do(serviceName, methodName, in, out)
249 if err != nil {
250 sleepIval := Backoff(i + 1)
251 ctx := logging.SetField(c.ctx, "retriesLeft", upto-i)
252 ctx = logging.SetField(ctx, "sleepFor", sleepIval)
253 if err != nil {
254 ctx = logging.SetField(ctx, "prevErr", err)
255 }
256 logging.Get(ctx).Warningf("retrying")
257 select {
258 case <-ctx.Done():
259 return
260 case <-time.After(sleepIval):
261 }
262 } else {
263 break
264 }
265 }
266 return
267 }
268
269 func mkHTTPDoer() (func(*http.Request) (*http.Response, error), func()) {
Vadim Sh. 2015/06/03 18:13:39 consider using https://github.com/luci/luci-go/tre
270 // TODO(riannucci): Figure out why the default client isn't good enough. Was
271 // seeing random hangups when running against appspot.com that the defau lt
272 // transport wasn't smart enough to deal with (e.g. would get Do calls w hich
273 // died with 'Connection Closed' errors.
274 transp := &http.Transport{
275 Dial: (&net.Dialer{
276 Timeout: 30 * time.Second,
277 KeepAlive: 30 * time.Second,
278 }).Dial,
279 TLSHandshakeTimeout: 10 * time.Second,
280 DisableKeepAlives: true,
281 }
282 ret := &http.Client{Transport: transp}
283 return ret.Do, transp.CloseIdleConnections
284 }
285
286 func (c *client) do(serviceName, methodName string, in interface{}, out interfac e{}) error {
287 do, closer := c.mkHTTPDoer()
288 defer closer()
289
290 tentry, err := c.getMethodInfo(serviceName, methodName)
291 if err != nil {
292 return err
293 }
294
295 if reflect.TypeOf(in) != tentry.reqType {
296 return fmt.Errorf("do(%s) type mismatch for input. Got %T but ex pected %s",
297 methodName, in, tentry.reqType)
298 }
299 // out must be a pointer to something.
300 if reflect.TypeOf(out) != tentry.rspType {
301 return fmt.Errorf("do(%s) type mismatch for output. Got %T but e xpected %s",
302 methodName, out, tentry.rspType)
303 }
304
305 var inBuf io.ReadWriter
306 if tentry.mi.HTTPMethod != "GET" {
307 inBuf = &bytes.Buffer{}
308 enc := json.NewEncoder(inBuf)
309 err := enc.Encode(in)
310 if err != nil {
311 return err
312 }
313 }
314
315 url, err := renderURL(c.url, tentry, in)
316 if err != nil {
317 return err
318 }
319
320 req, err := http.NewRequest(tentry.mi.HTTPMethod, url, inBuf)
321 if err != nil {
322 return err
323 }
324 if inBuf != nil {
325 req.Header.Add("content-type", "application/json")
326 }
327
328 rsp, err := do(req)
329 if err != nil {
330 return err
331 }
332 defer rsp.Body.Close()
333
334 data, err := ioutil.ReadAll(rsp.Body)
335 if err != nil {
336 return mark(fmt.Errorf("do(%s) failed (reading body): %s", metho dName, err))
337 }
338
339 if rsp.StatusCode/100 == 2 {
Vadim Sh. 2015/06/03 18:13:39 wat? Is rsp.StatusCode >= 200 && rsp.StatusCode <
340 if out != nil {
341 err = json.Unmarshal(data, out)
342 }
343 } else {
344 msg := &map[string]map[string]interface{}{}
345 err = json.Unmarshal(data, msg)
346 if err != nil {
347 return mark(fmt.Errorf("do(%s) failed (decoding json %q) : %s", methodName, string(data), err))
348 }
349
350 return mark(fmt.Errorf("do(%s) failed: %s", methodName, (*msg)[" error"]["message"]))
351 }
352 return nil
353 }
354
355 func (c *client) ForService(serviceName string) (ServiceClient, error) {
356 if c.serverDef.ServiceByName(serviceName) == nil {
357 return nil, mark(fmt.Errorf("getMethodInfo: no such service %s", serviceName))
358 }
359 return &serviceClient{c, serviceName}, nil
360 }
361
362 type serviceClient struct {
363 c Client
364 service string
365 }
366
367 var _ ServiceClient = (*serviceClient)(nil)
368
369 func (s *serviceClient) DoWithRetries(methodName string, in interface{}, out int erface{}, upto uint) error {
370 return s.c.DoWithRetries(s.service, methodName, in, out, upto)
371 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698