bilibili-backup/library/net/rpc/liverpc/client.go
2019-04-22 02:59:20 +00:00

335 lines
7.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package liverpc
import (
"context"
"fmt"
"net/url"
"sync/atomic"
"time"
"go-common/library/conf/env"
"go-common/library/log"
"go-common/library/naming"
"go-common/library/naming/discovery"
"go-common/library/net/metadata"
"go-common/library/net/trace"
"go-common/library/stat"
xtime "go-common/library/time"
"github.com/golang/protobuf/proto"
"github.com/pkg/errors"
)
// Key is ContextKey
type Key int
const (
_ Key = iota
// KeyHeader use this in context to pass rpc header field
// Depreated 请使用HeaderOption来传递Header
KeyHeader
// KeyTimeout deprecated
// Depreated 请使用HTTPOption来传递HTTP
KeyTimeout
)
const (
_scheme = "liverpc"
_dialRetries = 3
)
// Get Implement tracer carrier interface
func (m *Header) Get(key string) string {
if key == trace.KeyTraceID {
return m.TraceId
}
return ""
}
// Set Implement tracer carrier interface
func (m *Header) Set(key string, val string) {
if key == trace.KeyTraceID {
m.TraceId = val
}
}
var (
// ErrNoClient no RPC client.
errNoClient = errors.New("no rpc client")
errGroupInvalid = errors.New("invalid group")
stats = stat.RPCClient
)
// GroupAddrs a map struct storing addrs vary groups
type GroupAddrs map[string][]string
// ClientConfig client config.
type ClientConfig struct {
AppID string
Group string
Timeout xtime.Duration
ConnTimeout xtime.Duration
Addr string // if addr is provided, it will use add, else, use discovery
}
// Client is a RPC client.
type Client struct {
conf *ClientConfig
dis naming.Resolver
addrs atomic.Value // GroupAddrs
addrsIdx int64
}
// NewClient new a RPC client with discovery.
func NewClient(c *ClientConfig) *Client {
if c.Timeout <= 0 {
c.Timeout = xtime.Duration(time.Second)
}
if c.ConnTimeout <= 0 {
c.ConnTimeout = xtime.Duration(time.Second)
}
cli := &Client{
conf: c,
}
if c.Addr != "" {
groupAddrs := make(GroupAddrs)
groupAddrs[""] = []string{c.Addr}
cli.addrs.Store(groupAddrs)
return cli
}
cli.dis = discovery.Build(c.AppID)
// discovery watch & fetch nodes
event := cli.dis.Watch()
select {
case _, ok := <-event:
if !ok {
panic("刚启动就从discovery拉到了关闭的event")
}
cli.disFetch()
fmt.Printf("开始创建:%s 的liverpc client等待从discovery拉取节点%s\n", c.AppID, time.Now().Format("2006-01-02 15:04:05"))
case <-time.After(10 * time.Second):
fmt.Printf("失败创建:%s 的liverpc client竟然从discovery拉取节点超时了%s\n", c.AppID, time.Now().Format("2006-01-02 15:04:05"))
}
go cli.disproc(event)
return cli
}
func (c *Client) disproc(event <-chan struct{}) {
for {
_, ok := <-event
if !ok {
return
}
c.disFetch()
}
}
func (c *Client) disFetch() {
ins, ok := c.dis.Fetch(context.Background())
if !ok {
return
}
insZone, ok := ins[env.Zone]
if !ok {
return
}
addrs := make(GroupAddrs)
for _, svr := range insZone {
group, ok := svr.Metadata["color"]
if !ok {
group = ""
}
for _, addr := range svr.Addrs {
u, err := url.Parse(addr)
if err == nil && u.Scheme == _scheme {
addrs[group] = append(addrs[group], u.Host)
}
}
}
if len(addrs) > 0 {
c.addrs.Store(addrs)
}
}
// pickConn pick conn by addrs
func (c *Client) pickConn(ctx context.Context, addrs []string, dialTimeout time.Duration) (*ClientConn, error) {
var (
lastErr error
)
if len(addrs) == 0 {
lastErr = errors.New("addrs empty")
} else {
for i := 0; i < _dialRetries; i++ {
idx := atomic.AddInt64(&c.addrsIdx, 1)
addr := addrs[int(idx)%len(addrs)]
if dialTimeout == 0 {
dialTimeout = time.Duration(c.conf.ConnTimeout)
}
cc, err := Dial(ctx, "tcp", addr, time.Duration(c.conf.Timeout), dialTimeout)
if err != nil {
lastErr = errors.Wrapf(err, "Dial %s error", addr)
continue
}
return cc, nil
}
}
if lastErr != nil {
return nil, errors.WithMessage(errNoClient, lastErr.Error())
}
return nil, errors.WithStack(errNoClient)
}
// fetchAddrs fetch addrs by different strategies
// source_group first, come from request header if exists, currently only CallRaw supports source_group
// then env group, come from os.env
// since no invalid group found, return error
func (c *Client) fetchAddrs(ctx context.Context, request interface{}) (addrs []string, err error) {
var (
args *Args
groupAddrs GroupAddrs
ok bool
sourceGroup string
groups []string
)
defer func() {
if err != nil {
err = errors.WithMessage(errGroupInvalid, err.Error())
}
}()
// try parse request header and fetch source group
if args, ok = request.(*Args); ok && args.Header != nil {
sourceGroup = args.Header.SourceGroup
if sourceGroup != "" {
groups = append(groups, sourceGroup)
}
}
metaColor := metadata.String(ctx, metadata.Color)
if metaColor != "" && metaColor != sourceGroup {
groups = append(groups, metaColor)
}
if env.Color != "" && env.Color != metaColor {
groups = append(groups, env.Color)
}
groups = append(groups, "")
if groupAddrs, ok = c.addrs.Load().(GroupAddrs); !ok {
err = errors.New("addrs load error")
return
}
if len(groupAddrs) == 0 {
err = errors.New("group addrs empty")
return
}
for _, group := range groups {
if addrs, ok = groupAddrs[group]; ok {
break
}
}
if len(addrs) == 0 {
err = errors.Errorf("addrs empty source(%s), metadata(%s), env(%s), default empty, allAddrs(%+v)",
sourceGroup, metaColor, env.Color, groupAddrs)
return
}
return
}
// Call call the service method, waits for it to complete, and returns its error status.
// client: {service}
// serviceMethod: {version}|{controller.method}
// httpURL: /room/v1/Room/room_init
// httpURL: /{service}/{version}/{controller}/{method}
func (c *Client) Call(ctx context.Context, version int, serviceMethod string, in proto.Message, out proto.Message, opts ...CallOption) (err error) {
var (
cc *ClientConn
addrs []string
)
isPickErr := true
defer func() {
if cc != nil {
cc.Close()
}
if err != nil && isPickErr {
log.Error("liverpc Call pick connection error, version %d, method: %s, error: %+v", version, serviceMethod, err)
}
}() // for now it is non-persistent connection
var cInfo = &callInfo{}
for _, o := range opts {
o.before(cInfo)
}
addrs, err = c.fetchAddrs(ctx, in)
if err != nil {
return
}
cc, err = c.pickConn(ctx, addrs, cInfo.DialTimeout)
if err != nil {
return
}
isPickErr = false
cc.callInfo = cInfo
err = cc.Call(ctx, version, serviceMethod, in, out)
if err != nil {
return
}
for _, o := range opts {
o.after(cc.callInfo)
}
return
}
// CallRaw call the service method, waits for it to complete, and returns reply its error status.
// this is can be use without protobuf
// client: {service}
// serviceMethod: {version}|{controller.method}
// httpURL: /room/v1/Room/room_init
// httpURL: /{service}/{version}/{controller}/{method}
func (c *Client) CallRaw(ctx context.Context, version int, serviceMethod string, in *Args, opts ...CallOption) (out *Reply, err error) {
var (
cc *ClientConn
addrs []string
)
isPickErr := true
defer func() {
if cc != nil {
cc.Close()
}
if err != nil && isPickErr {
log.Error("liverpc CallRaw pick connection error, version %d, method: %s, error: %+v", version, serviceMethod, err)
}
}() // for now it is non-persistent connection
var cInfo = &callInfo{}
for _, o := range opts {
o.before(cInfo)
}
addrs, err = c.fetchAddrs(ctx, in)
if err != nil {
return
}
cc, err = c.pickConn(ctx, addrs, cInfo.DialTimeout)
if err != nil {
return
}
isPickErr = false
cc.callInfo = cInfo
out, err = cc.CallRaw(ctx, version, serviceMethod, in)
if err != nil {
return
}
for _, o := range opts {
o.after(cc.callInfo)
}
return
}
//Close handle client exit
func (c *Client) Close() {
if c.dis != nil {
c.dis.Close()
}
}