github.com/willyham/dosa@v2.3.1-0.20171024181418-1e446d37ee71+incompatible/connectors/yarpc/yarpc.go (about) 1 // Copyright (c) 2017 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package yarpc 22 23 import ( 24 "context" 25 "fmt" 26 "strings" 27 28 "os" 29 30 "github.com/pkg/errors" 31 "github.com/uber-go/dosa" 32 "github.com/uber-go/dosa/connectors/base" 33 dosarpc "github.com/uber/dosa-idl/.gen/dosa" 34 "github.com/uber/dosa-idl/.gen/dosa/dosaclient" 35 rpc "go.uber.org/yarpc" 36 "go.uber.org/yarpc/api/transport" 37 "go.uber.org/yarpc/transport/http" 38 "go.uber.org/yarpc/transport/tchannel" 39 ) 40 41 const ( 42 _version = "version" 43 _defaultServiceName = "dosa-gateway" 44 errCodeNotFound int32 = 404 45 errCodeAlreadyExists int32 = 409 46 errInvalidHandler string = "no handler for service" 47 errConnectionRefused string = "getsockopt: connection refused" 48 ) 49 50 // ErrInvalidHandler is used to help deliver a better error message when 51 // users have misconfigured the yarpc connector 52 type ErrInvalidHandler struct { 53 service string 54 scope string 55 } 56 57 // Error implements the error interface 58 func (e *ErrInvalidHandler) Error() string { 59 return fmt.Sprintf("the gateway %q refused to handle scope %q; probably because of a mismatch between gateway and scope in your configuration or initialization", e.service, e.scope) 60 } 61 62 // ErrorIsInvalidHandler check if the error is "ErrInvalidHandler" 63 func ErrorIsInvalidHandler(err error) bool { 64 return strings.Contains(err.Error(), errInvalidHandler) 65 } 66 67 // ErrConnectionRefused is used to help deliver a better error message when 68 // users have misconfigured the yarpc connector 69 type ErrConnectionRefused struct { 70 cause error 71 } 72 73 // Error implements the error interface 74 func (e *ErrConnectionRefused) Error() string { 75 return fmt.Sprintf("the gateway is not reachable, make sure the hostname and port are correct for your environment: %s", e.cause) 76 } 77 78 // ErrorIsConnectionRefused check if the error is "ErrConnectionRefused" 79 func ErrorIsConnectionRefused(err error) bool { 80 return strings.Contains(err.Error(), errConnectionRefused) 81 } 82 83 // Config contains the YARPC client parameters 84 type Config struct { 85 Transport string `yaml:"transport"` 86 Host string `yaml:"host"` 87 Port string `yaml:"port"` 88 CallerName string `yaml:"callerName"` 89 ServiceName string `yaml:"serviceName"` 90 ClientConfig *transport.ClientConfig `yaml:"clientConfig"` 91 } 92 93 // Connector holds the client-side RPC interface and some schema information 94 type Connector struct { 95 base.Connector 96 Client dosaclient.Interface 97 Config *Config 98 dispatcher *rpc.Dispatcher 99 } 100 101 // NewConnectorWithTransport creates a new instance with user provided transport 102 func NewConnectorWithTransport(cc transport.ClientConfig) *Connector { 103 client := dosaclient.New(cc) 104 config := &Config{ 105 CallerName: cc.Caller(), 106 ServiceName: cc.Service(), 107 ClientConfig: &cc, 108 } 109 return &Connector{ 110 Client: client, 111 Config: config, 112 } 113 } 114 115 // NewConnectorWithChannel creates a new instance using the given tchannel. 116 // Note that the method refers to the YARPC tchannel interface which should 117 // be satisfied in order to be used with this connector (and YARPC). Use this 118 // if you are using a raw TChannel configuration instead of YARPC directly. 119 func NewConnectorWithChannel(ch tchannel.Channel) (*Connector, error) { 120 ts, err := tchannel.NewChannelTransport(tchannel.WithChannel(ch)) 121 if err != nil { 122 return nil, err 123 } 124 // use the channel's service name for "caller" and use default for outbound 125 // configuration since we just need a consistent mapping to get client. 126 ycfg := rpc.Config{ 127 Name: ts.Channel().ServiceName(), 128 Outbounds: rpc.Outbounds{ 129 _defaultServiceName: { 130 Unary: ts.NewOutbound(), 131 }, 132 }, 133 } 134 135 dispatcher := rpc.NewDispatcher(ycfg) 136 if err := dispatcher.Start(); err != nil { 137 // this should never happen since we're always providing sane defaults 138 return nil, err 139 } 140 141 client := dosaclient.New(dispatcher.ClientConfig(_defaultServiceName)) 142 config := &Config{ 143 CallerName: ycfg.Name, 144 ServiceName: _defaultServiceName, 145 } 146 return &Connector{ 147 Client: client, 148 Config: config, 149 dispatcher: dispatcher, 150 }, nil 151 } 152 153 // NewConnector returns a new YARPC connector with the given configuration. 154 func NewConnector(cfg *Config) (*Connector, error) { 155 ycfg := rpc.Config{Name: cfg.CallerName} 156 157 // host and port are required 158 if cfg.Host == "" { 159 return nil, errors.New("invalid host") 160 } 161 162 if cfg.Port == "" { 163 return nil, errors.New("invalid port") 164 } 165 166 // service name is not required, defaults to "dosa-gateway" 167 if cfg.ServiceName == "" { 168 cfg.ServiceName = _defaultServiceName 169 } 170 171 switch cfg.Transport { 172 case "http": 173 uri := fmt.Sprintf("http://%s:%s", cfg.Host, cfg.Port) 174 ts := http.NewTransport() 175 ycfg.Outbounds = rpc.Outbounds{ 176 cfg.ServiceName: { 177 Unary: ts.NewSingleOutbound(uri), 178 }, 179 } 180 case "tchannel": 181 hostPort := fmt.Sprintf("%s:%s", cfg.Host, cfg.Port) 182 // this looks wrong, BUT since it's a uni-directional tchannel 183 // connection, we have to pass CallerName as the tchannel "ServiceName" 184 // for source/destination to be reported correctly by RPC layer. 185 ts, err := tchannel.NewChannelTransport(tchannel.ServiceName(cfg.CallerName)) 186 if err != nil { 187 return nil, err 188 } 189 ycfg.Outbounds = rpc.Outbounds{ 190 cfg.ServiceName: { 191 Unary: ts.NewSingleOutbound(hostPort), 192 }, 193 } 194 default: 195 return nil, errors.New("invalid transport (only http or tchannel supported)") 196 } 197 198 // important to note that this will panic if config contains invalid 199 // values such as service name containing invalid characters 200 dispatcher := rpc.NewDispatcher(ycfg) 201 if err := dispatcher.Start(); err != nil { 202 return nil, err 203 } 204 205 client := dosaclient.New(dispatcher.ClientConfig(cfg.ServiceName)) 206 return &Connector{ 207 Client: client, 208 Config: cfg, 209 dispatcher: dispatcher, 210 }, nil 211 } 212 213 // CreateIfNotExists ... 214 func (c *Connector) CreateIfNotExists(ctx context.Context, ei *dosa.EntityInfo, values map[string]dosa.FieldValue) error { 215 ev, err := fieldValueMapFromClientMap(values) 216 if err != nil { 217 return err 218 } 219 createRequest := dosarpc.CreateRequest{ 220 Ref: entityInfoToSchemaRef(ei), 221 EntityValues: ev, 222 } 223 224 err = c.Client.CreateIfNotExists(ctx, &createRequest, VersionHeader()) 225 if err != nil { 226 if be, ok := err.(*dosarpc.BadRequestError); ok { 227 if be.ErrorCode != nil && *be.ErrorCode == errCodeAlreadyExists { 228 return errors.Wrap(&dosa.ErrAlreadyExists{}, "failed to create") 229 } 230 } 231 } 232 return errors.Wrap(err, "failed to create") 233 } 234 235 // Upsert inserts or updates your data 236 func (c *Connector) Upsert(ctx context.Context, ei *dosa.EntityInfo, values map[string]dosa.FieldValue) error { 237 ev, err := fieldValueMapFromClientMap(values) 238 if err != nil { 239 return err 240 } 241 upsertRequest := dosarpc.UpsertRequest{ 242 Ref: entityInfoToSchemaRef(ei), 243 EntityValues: ev, 244 } 245 return c.Client.Upsert(ctx, &upsertRequest, VersionHeader()) 246 } 247 248 // Read reads a single entity 249 func (c *Connector) Read(ctx context.Context, ei *dosa.EntityInfo, keys map[string]dosa.FieldValue, minimumFields []string) (map[string]dosa.FieldValue, error) { 250 // Convert the fields from the client's map to a set of fields to read 251 var rpcMinimumFields map[string]struct{} 252 if minimumFields != nil { 253 rpcMinimumFields = map[string]struct{}{} 254 for _, field := range minimumFields { 255 rpcMinimumFields[field] = struct{}{} 256 } 257 } 258 259 // convert the key values from interface{} to RPC's Value 260 rpcFields := make(dosarpc.FieldValueMap) 261 for key, value := range keys { 262 rv, err := RawValueFromInterface(value) 263 if err != nil { 264 return nil, errors.Wrapf(err, "Key field %q", key) 265 } 266 if rv == nil { 267 continue 268 } 269 rpcValue := &dosarpc.Value{ElemValue: rv} 270 rpcFields[key] = rpcValue 271 } 272 273 // perform the read request 274 readRequest := &dosarpc.ReadRequest{ 275 Ref: entityInfoToSchemaRef(ei), 276 KeyValues: rpcFields, 277 FieldsToRead: rpcMinimumFields, 278 } 279 280 response, err := c.Client.Read(ctx, readRequest, VersionHeader()) 281 if err != nil { 282 if be, ok := err.(*dosarpc.BadRequestError); ok { 283 if be.ErrorCode != nil && *be.ErrorCode == errCodeNotFound { 284 return nil, errors.Wrap(&dosa.ErrNotFound{}, "Read failed: not found") 285 } 286 } 287 return nil, errors.Wrap(err, "Read failed") 288 } 289 290 // no error, so for each column, transform it into the map of (col->value) items 291 292 return decodeResults(ei, response.EntityValues), nil 293 } 294 295 // MultiRead reads multiple entities at one time 296 func (c *Connector) MultiRead(ctx context.Context, ei *dosa.EntityInfo, keys []map[string]dosa.FieldValue, minimumFields []string) ([]*dosa.FieldValuesOrError, error) { 297 // Convert the fields from the client's map to a set of fields to read 298 rpcMinimumFields := makeRPCminimumFields(minimumFields) 299 300 // convert the keys to RPC's Value 301 rpcFields := make([]dosarpc.FieldValueMap, len(keys)) 302 for i, kmap := range keys { 303 rpcFields[i] = make(dosarpc.FieldValueMap) 304 for key, value := range kmap { 305 rv, err := RawValueFromInterface(value) 306 if err != nil { 307 return nil, err 308 } 309 if rv == nil { 310 continue 311 } 312 rpcValue := &dosarpc.Value{ElemValue: rv} 313 rpcFields[i][key] = rpcValue 314 } 315 } 316 317 // perform the multi read request 318 request := &dosarpc.MultiReadRequest{ 319 Ref: entityInfoToSchemaRef(ei), 320 KeyValues: rpcFields, 321 FieldsToRead: rpcMinimumFields, 322 } 323 324 response, err := c.Client.MultiRead(ctx, request, VersionHeader()) 325 if err != nil { 326 return nil, errors.Wrap(err, "MultiRead failed") 327 } 328 329 rpcResults := response.Results 330 results := make([]*dosa.FieldValuesOrError, len(rpcResults)) 331 for i, rpcResult := range rpcResults { 332 results[i] = &dosa.FieldValuesOrError{Values: make(map[string]dosa.FieldValue), Error: nil} 333 for name, value := range rpcResult.EntityValues { 334 for _, col := range ei.Def.Columns { 335 if col.Name == name { 336 results[i].Values[name] = RawValueAsInterface(*value.ElemValue, col.Type) 337 break 338 } 339 } 340 } 341 if rpcResult.Error != nil { 342 // TODO check other fields in the thrift error object such as ShouldRetry 343 results[i].Error = errors.New(*rpcResult.Error.Msg) 344 } 345 } 346 347 return results, nil 348 } 349 350 // MultiUpsert is not yet implemented 351 func (c *Connector) MultiUpsert(ctx context.Context, ei *dosa.EntityInfo, multiValues []map[string]dosa.FieldValue) ([]error, error) { 352 panic("not implemented") 353 } 354 355 // Remove marshals a request to the YARPC remove call 356 func (c *Connector) Remove(ctx context.Context, ei *dosa.EntityInfo, keys map[string]dosa.FieldValue) error { 357 // convert the key values from interface{} to RPC's Value 358 rpcFields := make(dosarpc.FieldValueMap) 359 for key, value := range keys { 360 rv, err := RawValueFromInterface(value) 361 if err != nil { 362 return errors.Wrapf(err, "Key field %q", key) 363 } 364 if rv == nil { 365 continue 366 } 367 rpcValue := &dosarpc.Value{ElemValue: rv} 368 rpcFields[key] = rpcValue 369 } 370 371 // perform the remove request 372 removeRequest := &dosarpc.RemoveRequest{ 373 Ref: entityInfoToSchemaRef(ei), 374 KeyValues: rpcFields, 375 } 376 377 err := c.Client.Remove(ctx, removeRequest, VersionHeader()) 378 if err != nil { 379 return errors.Wrap(err, "Remove failed") 380 } 381 return nil 382 } 383 384 // RemoveRange removes all entities within the range specified by the columnConditions. 385 func (c *Connector) RemoveRange(ctx context.Context, ei *dosa.EntityInfo, columnConditions map[string][]*dosa.Condition) error { 386 rpcConditions, err := createRPCConditions(columnConditions) 387 if err != nil { 388 return errors.Wrap(err, "RemoveRange failed: invalid column conditions") 389 } 390 391 request := &dosarpc.RemoveRangeRequest{ 392 Ref: entityInfoToSchemaRef(ei), 393 Conditions: rpcConditions, 394 } 395 396 if err := c.Client.RemoveRange(ctx, request, VersionHeader()); err != nil { 397 return errors.Wrap(err, "RemoveRange failed") 398 } 399 return nil 400 } 401 402 // MultiRemove is not yet implemented 403 func (c *Connector) MultiRemove(ctx context.Context, ei *dosa.EntityInfo, multiKeys []map[string]dosa.FieldValue) ([]error, error) { 404 panic("not implemented") 405 } 406 407 // Range does a scan across a range 408 func (c *Connector) Range(ctx context.Context, ei *dosa.EntityInfo, columnConditions map[string][]*dosa.Condition, minimumFields []string, token string, limit int) ([]map[string]dosa.FieldValue, string, error) { 409 limit32 := int32(limit) 410 rpcMinimumFields := makeRPCminimumFields(minimumFields) 411 rpcConditions, err := createRPCConditions(columnConditions) 412 if err != nil { 413 return nil, "", errors.Wrap(err, "Range failed: invalid column conditions") 414 } 415 rangeRequest := dosarpc.RangeRequest{ 416 Ref: entityInfoToSchemaRef(ei), 417 Token: &token, 418 Limit: &limit32, 419 Conditions: rpcConditions, 420 FieldsToRead: rpcMinimumFields, 421 } 422 response, err := c.Client.Range(ctx, &rangeRequest, VersionHeader()) 423 if err != nil { 424 return nil, "", errors.Wrap(err, "Range failed") 425 } 426 results := []map[string]dosa.FieldValue{} 427 for _, entity := range response.Entities { 428 results = append(results, decodeResults(ei, entity)) 429 } 430 return results, *response.NextToken, nil 431 } 432 433 func createRPCConditions(columnConditions map[string][]*dosa.Condition) ([]*dosarpc.Condition, error) { 434 rpcConditions := []*dosarpc.Condition{} 435 for field, conditions := range columnConditions { 436 // Warning: Don't remove this line. 437 // field variable always has the same address. If we want to dereference it, we have to assign the value to a new variable. 438 fieldName := field 439 for _, condition := range conditions { 440 rv, err := RawValueFromInterface(condition.Value) 441 if err != nil { 442 return nil, errors.Wrap(err, "Bad range value") 443 } 444 if rv == nil { 445 continue 446 } 447 rpcConditions = append(rpcConditions, &dosarpc.Condition{ 448 Op: encodeOperator(condition.Op), 449 Field: &dosarpc.Field{Name: &fieldName, Value: &dosarpc.Value{ElemValue: rv}}, 450 }) 451 } 452 } 453 454 return rpcConditions, nil 455 } 456 457 // Scan marshals a scan request into YARPC 458 func (c *Connector) Scan(ctx context.Context, ei *dosa.EntityInfo, minimumFields []string, token string, limit int) ([]map[string]dosa.FieldValue, string, error) { 459 limit32 := int32(limit) 460 rpcMinimumFields := makeRPCminimumFields(minimumFields) 461 scanRequest := dosarpc.ScanRequest{ 462 Ref: entityInfoToSchemaRef(ei), 463 Token: &token, 464 Limit: &limit32, 465 FieldsToRead: rpcMinimumFields, 466 } 467 response, err := c.Client.Scan(ctx, &scanRequest, VersionHeader()) 468 if err != nil { 469 return nil, "", errors.Wrap(err, "Scan failed") 470 } 471 results := []map[string]dosa.FieldValue{} 472 for _, entity := range response.Entities { 473 results = append(results, decodeResults(ei, entity)) 474 } 475 return results, *response.NextToken, nil 476 } 477 478 // CheckSchema is one way to register a set of entities. This can be further validated by 479 // a schema service downstream. 480 func (c *Connector) CheckSchema(ctx context.Context, scope, namePrefix string, eds []*dosa.EntityDefinition) (int32, error) { 481 // convert the client EntityDefinition to the RPC EntityDefinition 482 rpcEntityDefinition := make([]*dosarpc.EntityDefinition, len(eds)) 483 for i, ed := range eds { 484 rpcEntityDefinition[i] = EntityDefinitionToThrift(ed) 485 } 486 csr := dosarpc.CheckSchemaRequest{EntityDefs: rpcEntityDefinition, Scope: &scope, NamePrefix: &namePrefix} 487 488 response, err := c.Client.CheckSchema(ctx, &csr, VersionHeader()) 489 490 if err != nil { 491 return dosa.InvalidVersion, wrapError(err, "CheckSchema failed", scope, c.Config.ServiceName) 492 } 493 494 return *response.Version, nil 495 } 496 497 // UpsertSchema upserts the schema through RPC 498 func (c *Connector) UpsertSchema(ctx context.Context, scope, namePrefix string, eds []*dosa.EntityDefinition) (*dosa.SchemaStatus, error) { 499 rpcEds := make([]*dosarpc.EntityDefinition, len(eds)) 500 for i, ed := range eds { 501 rpcEds[i] = EntityDefinitionToThrift(ed) 502 } 503 504 request := &dosarpc.UpsertSchemaRequest{ 505 Scope: &scope, 506 NamePrefix: &namePrefix, 507 EntityDefs: rpcEds, 508 } 509 510 response, err := c.Client.UpsertSchema(ctx, request, VersionHeader()) 511 if err != nil { 512 return nil, wrapError(err, "UpsertSchema failed", scope, c.Config.ServiceName) 513 } 514 515 status := "" 516 if response.Status != nil { 517 status = *response.Status 518 } 519 520 if response.Version == nil { 521 return nil, errors.New("UpsertSchema failed: server returns version nil") 522 } 523 524 return &dosa.SchemaStatus{ 525 Version: *response.Version, 526 Status: status, 527 }, nil 528 } 529 530 // CheckSchemaStatus checks the status of specific version of schema 531 func (c *Connector) CheckSchemaStatus(ctx context.Context, scope, namePrefix string, version int32) (*dosa.SchemaStatus, error) { 532 request := dosarpc.CheckSchemaStatusRequest{Scope: &scope, NamePrefix: &namePrefix, Version: &version} 533 response, err := c.Client.CheckSchemaStatus(ctx, &request, VersionHeader()) 534 535 if err != nil { 536 return nil, wrapError(err, "ChecksShemaStatus failed", scope, c.Config.ServiceName) 537 } 538 539 status := "" 540 if response.Status != nil { 541 status = *response.Status 542 } 543 544 if response.Version == nil { 545 return nil, errors.New("ChecksShemaStatus failed: server returns version nil") 546 } 547 548 return &dosa.SchemaStatus{ 549 Version: *response.Version, 550 Status: status, 551 }, nil 552 } 553 554 // CreateScope creates the scope specified 555 func (c *Connector) CreateScope(ctx context.Context, scope string) error { 556 request := &dosarpc.CreateScopeRequest{ 557 Name: &scope, 558 } 559 560 if err := c.Client.CreateScope(ctx, request, VersionHeader()); err != nil { 561 return errors.Wrap(err, "CreateScope failed") 562 } 563 564 return nil 565 } 566 567 // TruncateScope truncates all data in the scope specified 568 func (c *Connector) TruncateScope(ctx context.Context, scope string) error { 569 request := &dosarpc.TruncateScopeRequest{ 570 Name: &scope, 571 } 572 573 if err := c.Client.TruncateScope(ctx, request, VersionHeader()); err != nil { 574 return errors.Wrap(err, "TruncateScope failed") 575 } 576 577 return nil 578 } 579 580 // DropScope removes the scope specified 581 func (c *Connector) DropScope(ctx context.Context, scope string) error { 582 request := &dosarpc.DropScopeRequest{ 583 Name: &scope, 584 } 585 586 if err := c.Client.DropScope(ctx, request, VersionHeader()); err != nil { 587 return errors.Wrap(err, "DropScope failed") 588 } 589 590 return nil 591 } 592 593 // ScopeExists is not implemented yet 594 func (c *Connector) ScopeExists(ctx context.Context, scope string) (bool, error) { 595 panic("not implemented") 596 } 597 598 // Shutdown stops the dispatcher and drains client 599 func (c *Connector) Shutdown() error { 600 // instances w/ mocked client shouldn't require a dispatcher 601 if c.dispatcher == nil { 602 return nil 603 } 604 return c.dispatcher.Stop() 605 } 606 607 func wrapError(err error, message, scope, service string) error { 608 if ErrorIsInvalidHandler(err) { 609 err = &ErrInvalidHandler{scope: scope, service: service} 610 } 611 if ErrorIsConnectionRefused(err) { 612 err = &ErrConnectionRefused{err} 613 } 614 return errors.Wrap(err, message) 615 } 616 617 func getWithDefault(args map[string]interface{}, elem string, def string) string { 618 v, ok := args[elem] 619 if ok { 620 return v.(string) 621 } 622 return def 623 624 } 625 626 func init() { 627 dosa.RegisterConnector("yarpc", func(args dosa.CreationArgs) (dosa.Connector, error) { 628 host, ok := args["host"] 629 if !ok { 630 return nil, errors.New("Missing connector host value") 631 } 632 633 port, ok := args["port"] 634 if !ok { 635 return nil, errors.New("Missing connector port value") 636 } 637 638 trans := getWithDefault(args, "transport", "tchannel") 639 callername := getWithDefault(args, "callername", os.Getenv("USER")) 640 servicename := getWithDefault(args, "servicename", "test") 641 cfg := Config{ 642 Transport: trans, 643 Host: host.(string), 644 Port: port.(string), 645 CallerName: callername, 646 ServiceName: servicename, 647 } 648 return NewConnector(&cfg) 649 }) 650 }