Chromium Code Reviews| 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 |