OLD | NEW |
---|---|
(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 } | |
OLD | NEW |