Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

21
app/infra/discovery/BUILD Normal file
View File

@@ -0,0 +1,21 @@
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/infra/discovery/cmd:all-srcs",
"//app/infra/discovery/conf:all-srcs",
"//app/infra/discovery/dao:all-srcs",
"//app/infra/discovery/http:all-srcs",
"//app/infra/discovery/model:all-srcs",
"//app/infra/discovery/service:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,77 @@
### discovery
#### Version 1.8.4
> 1.disocoery不区分环境
#### Version 1.8.3
> 1.修复自发现按配置zone进行同步
#### Version 1.8.2
> 1.添加zones种子节点同步
#### Version 1.8.1
> 1.修复自发现zone过滤其他机房
#### Version 1.8.0
1. 迁移infra
#### Version 1.7.1
> 1.添加自己发现discovery节点
#### Version 1.7.0
> 1.refactor set
> 2.set metadata
> 3.remove color
#### Version 1.6.3
> 1.修复同一个hostname多个长连接时串ch的bug
#### Version 1.6.2
> 1.删除兼容的http rpc字段
#### Version 1.6.1
> 1.增加polling查看当前正在poll的host
#### Version 1.6.0
> 1.添加批量fetch接口
> 2.移动weight到metadata
#### Version 1.5.0
> 1.优化修改app存储结构
> 2.去除treeid兼容
#### Version 1.4.4
1. 更新时返回全部zone
#### Version 1.4.3
1. 修复broadcast 没下发zoneinstances
#### Version 1.4.2
1. 修复latest_timestamp 更新
#### Verson 1.4.1
1. 区分zone返回instances
#### Version 1.4.0
1. 支持返回多个zone实例
#### Version 1.3.1
1. 修复同时注册更新时间相同导致的304修改精度为纳秒
#### Version 1.3.0
1. 支持多注册中心数据同步
#### Version 1.2.0
1. 增加treeid
2. 增加polls 批量订阅
3. 增加chan连接池
4. 使用 "部门.分组" 为key减小锁粒度
#### Version 1.1.0
1. 删除replication多余参数。
2. 完善单测mock http请求覆盖replication
3. poll新增host字段每次poll请求结束后删除conn
#### Version 1.0.0
1. 支持结点同步注册,心跳,取消请求。
2. 支持服务自我保护,防止网络闪断。
3. 支持长轮询更新服务变化。
4. 支持心跳同步过程中根据dirtytime纠正同步数据。

View File

@@ -0,0 +1,11 @@
# Owner
haoguanwei
lintanghui
# Author
chenshangqiang
lintanghui
# Reviewer
maojian
haoguanwei

View File

@@ -0,0 +1,16 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- chenshangqiang
- haoguanwei
- lintanghui
labels:
- infra
- infra/discovery
options:
no_parent_owners: true
reviewers:
- chenshangqiang
- haoguanwei
- lintanghui
- maojian

View File

@@ -0,0 +1,16 @@
# go-common/app/infra/discoveryovery
##### 项目简介
> 1. 服务注册发现
> 2. AP系统,不依赖任何组件或中间价,保证高可用
##### 编译环境
> 1. 请只用golang v1.7.x以上版本编译执行。
##### 依赖包
> 1. 公共依赖
##### 编译执行
> 1. 启动执行
> 2. 项目文档http://info.bilibili.co/pages/viewpage.action?pageId=1741193

View File

@@ -0,0 +1,48 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
)
go_binary(
name = "cmd",
embed = [":go_default_library"],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = ["main.go"],
data = [
"discovery-example.toml",
"discovery-idc1-1.toml",
"discovery-idc1-2.toml",
"discovery-idc2-1.toml",
"discovery-idc2-2.toml",
],
importpath = "go-common/app/infra/discovery/cmd",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/infra/discovery/conf:go_default_library",
"//app/infra/discovery/http:go_default_library",
"//app/infra/discovery/service:go_default_library",
"//library/log:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,52 @@
version = "1.0.0"
user = "nobody"
pid = "/tmp/discovery.pid"
dir = "./"
perf = "127.0.0.1:7170"
nodes = ["10.23.137.45:7171", "127.0.0.1:7173"]
[zones]
"127.0.0.1:7173"="zone1"
[log]
dir = "/data/log/discovery/"
[bm]
[bm.inner]
addr = "0.0.0.0:7171"
timeout = "40s"
[identify]
[identify.host]
auth = "http://passport.bilibili.co"
secret = "http://open.bilibili.co"
[identify.httpClient]
key = "0c4b8fe3ff35a4b6"
secret = "b370880d1aca7d3a289b9b9a7f4d6812"
dial = "30ms"
timeout = "100ms"
keepAlive = "60s"
[identify.httpClient.breaker]
window = "10s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[identify.httpClient.url]
"http://passport.bilibili.co/intranet/auth/tokenInfo" = {timeout = "100ms"}
"http://passport.bilibili.co/intranet/auth/cookieInfo" = {timeout = "100ms"}
"http://open.bilibili.co/api/getsecret" = {timeout = "500ms"}
[httpClient]
key = "0c4b8fe3ff35a4b6"
secret = "b370880d1aca7d3a289b9b9a7f4d6812"
dial = "500ms"
timeout = "1s"
keepAlive = "60s"
[httpClient.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100

View File

@@ -0,0 +1,51 @@
version = "1.0.0"
user = "nobody"
pid = "/tmp/discovery.pid"
dir = "./"
nodes = ["127.0.0.1:7171", "127.0.0.1:7172"]
[zones]
"127.0.0.1:7181"="zone2"
[log]
dir = "/data/log/discovery/"
[bm]
[bm.inner]
addr = "0.0.0.0:7171"
timeout = "40s"
[identify]
[identify.host]
auth = "http://passport.bilibili.co"
secret = "http://open.bilibili.co"
[identify.httpClient]
key = "0c4b8fe3ff35a4b6"
secret = "b370880d1aca7d3a289b9b9a7f4d6812"
dial = "30ms"
timeout = "100ms"
keepAlive = "60s"
[identify.httpClient.breaker]
window = "10s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[identify.httpClient.url]
"http://passport.bilibili.co/intranet/auth/tokenInfo" = {timeout = "100ms"}
"http://passport.bilibili.co/intranet/auth/cookieInfo" = {timeout = "100ms"}
"http://open.bilibili.co/api/getsecret" = {timeout = "500ms"}
[httpClient]
key = "0c4b8fe3ff35a4b6"
secret = "b370880d1aca7d3a289b9b9a7f4d6812"
dial = "500ms"
timeout = "1s"
keepAlive = "60s"
[httpClient.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100

View File

@@ -0,0 +1,51 @@
version = "1.0.0"
user = "nobody"
pid = "/tmp/discovery.pid"
dir = "./"
nodes = ["127.0.0.1:7171", "127.0.0.1:7172"]
[zones]
"127.0.0.1:7181"="zone2"
[log]
dir = "/data/log/discovery/"
[bm]
[bm.inner]
addr = "0.0.0.0:7172"
timeout = "40s"
[identify]
[identify.host]
auth = "http://passport.bilibili.co"
secret = "http://open.bilibili.co"
[identify.httpClient]
key = "0c4b8fe3ff35a4b6"
secret = "b370880d1aca7d3a289b9b9a7f4d6812"
dial = "30ms"
timeout = "100ms"
keepAlive = "60s"
[identify.httpClient.breaker]
window = "10s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[identify.httpClient.url]
"http://passport.bilibili.co/intranet/auth/tokenInfo" = {timeout = "100ms"}
"http://passport.bilibili.co/intranet/auth/cookieInfo" = {timeout = "100ms"}
"http://open.bilibili.co/api/getsecret" = {timeout = "500ms"}
[httpClient]
key = "0c4b8fe3ff35a4b6"
secret = "b370880d1aca7d3a289b9b9a7f4d6812"
dial = "500ms"
timeout = "1s"
keepAlive = "60s"
[httpClient.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100

View File

@@ -0,0 +1,51 @@
version = "1.0.0"
user = "nobody"
pid = "/tmp/discovery.pid"
dir = "./"
nodes = ["127.0.0.1:7181", "127.0.0.1:7182"]
[zones]
"127.0.0.1:7171"="zone1"
[log]
dir = "/data/log/discovery/"
[bm]
[bm.inner]
addr = "0.0.0.0:7181"
timeout = "40s"
[identify]
[identify.host]
auth = "http://passport.bilibili.co"
secret = "http://open.bilibili.co"
[identify.httpClient]
key = "0c4b8fe3ff35a4b6"
secret = "b370880d1aca7d3a289b9b9a7f4d6812"
dial = "30ms"
timeout = "100ms"
keepAlive = "60s"
[identify.httpClient.breaker]
window = "10s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[identify.httpClient.url]
"http://passport.bilibili.co/intranet/auth/tokenInfo" = {timeout = "100ms"}
"http://passport.bilibili.co/intranet/auth/cookieInfo" = {timeout = "100ms"}
"http://open.bilibili.co/api/getsecret" = {timeout = "500ms"}
[httpClient]
key = "0c4b8fe3ff35a4b6"
secret = "b370880d1aca7d3a289b9b9a7f4d6812"
dial = "500ms"
timeout = "1s"
keepAlive = "60s"
[httpClient.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100

View File

@@ -0,0 +1,51 @@
version = "1.0.0"
user = "nobody"
pid = "/tmp/discovery.pid"
dir = "./"
nodes = ["127.0.0.1:7181", "127.0.0.1:7182"]
[zones]
"127.0.0.1:7171"="zone1"
[log]
dir = "/data/log/discovery/"
[bm]
[bm.inner]
addr = "0.0.0.0:7182"
timeout = "40s"
[identify]
[identify.host]
auth = "http://passport.bilibili.co"
secret = "http://open.bilibili.co"
[identify.httpClient]
key = "0c4b8fe3ff35a4b6"
secret = "b370880d1aca7d3a289b9b9a7f4d6812"
dial = "30ms"
timeout = "100ms"
keepAlive = "60s"
[identify.httpClient.breaker]
window = "10s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[identify.httpClient.url]
"http://passport.bilibili.co/intranet/auth/tokenInfo" = {timeout = "100ms"}
"http://passport.bilibili.co/intranet/auth/cookieInfo" = {timeout = "100ms"}
"http://open.bilibili.co/api/getsecret" = {timeout = "500ms"}
[httpClient]
key = "0c4b8fe3ff35a4b6"
secret = "b370880d1aca7d3a289b9b9a7f4d6812"
dial = "500ms"
timeout = "1s"
keepAlive = "60s"
[httpClient.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100

View File

@@ -0,0 +1,48 @@
package main
import (
"flag"
"math/rand"
"os"
"os/signal"
"syscall"
"time"
"go-common/app/infra/discovery/conf"
"go-common/app/infra/discovery/http"
"go-common/app/infra/discovery/service"
"go-common/library/log"
)
func main() {
flag.Parse()
if err := conf.Init(); err != nil {
log.Error("conf.Init() error(%v)", err)
panic(err)
}
// init log
log.Init(conf.Conf.Log)
defer log.Close()
log.Info("discovery start")
// service init
rand.Seed(time.Now().UnixNano())
svc, cancel := service.New(conf.Conf)
http.Init(conf.Conf, svc)
// init signal
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
for {
s := <-c
log.Info("discovery get a signal %s", s.String())
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
cancel()
time.Sleep(time.Second)
log.Info("discovery exit")
return
case syscall.SIGHUP:
default:
return
}
}
}

View File

@@ -0,0 +1,35 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["conf.go"],
importpath = "go-common/app/infra/discovery/conf",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/conf:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/ip:go_default_library",
"//vendor/github.com/BurntSushi/toml:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,106 @@
package conf
import (
"errors"
"flag"
"net"
"go-common/library/conf"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
xip "go-common/library/net/ip"
"github.com/BurntSushi/toml"
)
var (
confPath string
client *conf.Client
// Conf conf
Conf = &Config{}
// ConfCh for update node of server.
ConfCh = make(chan struct{}, 1)
configKey = "discovery-service.toml"
)
// Config config
type Config struct {
Nodes []string
Zones map[string][]string // zone -> nodes
BM *HTTPServers
Log *log.Config
HTTPClient *bm.ClientConfig
}
func (c *Config) fix() (err error) {
// check ip
host, port, err := net.SplitHostPort(c.BM.Inner.Addr)
if err != nil {
return
}
if host == "0.0.0.0" || host == "127.0.0.1" || host == "" {
host = xip.InternalIP()
}
c.BM.Inner.Addr = host + ":" + port
return
}
// HTTPServers Http Servers
type HTTPServers struct {
Inner *bm.ServerConfig
}
func init() {
// flag.StringVar(&confPath, "conf", "discovery-example.toml", "config path")
flag.StringVar(&confPath, "conf", "", "config path")
}
// Init init conf
func Init() (err error) {
if confPath != "" {
if _, err = toml.DecodeFile(confPath, &Conf); err != nil {
return
}
return Conf.fix()
}
err = remote()
return
}
func remote() (err error) {
if client, err = conf.New(); err != nil {
return
}
if err = load(); err != nil {
return
}
go func() {
for range client.Event() {
log.Info("config reload")
if load() != nil {
log.Error("config reload error (%v)", err)
continue
}
// to change the node of server
ConfCh <- struct{}{}
}
}()
return
}
func load() (err error) {
s, ok := client.Value(configKey)
if !ok {
return errors.New("load config center error")
}
var tmpConf *Config
if _, err = toml.Decode(s, &tmpConf); err != nil {
return errors.New("could not decode config")
}
if err = tmpConf.fix(); err != nil {
return
}
// copy
*Conf = *tmpConf
return
}

View File

@@ -0,0 +1,66 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"guard_test.go",
"node_test.go",
"nodes_test.go",
"registry_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/infra/discovery/conf:go_default_library",
"//app/infra/discovery/model:go_default_library",
"//library/ecode:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/netutil/breaker:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
"//vendor/gopkg.in/h2non/gock.v1:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"guard.go",
"node.go",
"nodes.go",
"registry.go",
],
importpath = "go-common/app/infra/discovery/dao",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/infra/discovery/conf:go_default_library",
"//app/infra/discovery/model:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/sync/errgroup:go_default_library",
"//library/xstr:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,60 @@
package dao
import (
"sync"
"sync/atomic"
"go-common/library/log"
)
const (
_percentThreshold float64 = 0.85
)
// Guard count the renew of all operations for self protection
type Guard struct {
expPerMin int64
expThreshold int64
facInMin int64
facLastMin int64
lock sync.RWMutex
}
func (g *Guard) setExp(cnt int64) {
g.lock.Lock()
g.expPerMin = cnt * 2
g.expThreshold = int64(float64(g.expPerMin) * _percentThreshold)
g.lock.Unlock()
}
func (g *Guard) incrExp() {
g.lock.Lock()
g.expPerMin = g.expPerMin + 2
g.expThreshold = int64(float64(g.expPerMin) * _percentThreshold)
g.lock.Unlock()
}
func (g *Guard) updateFac() {
atomic.StoreInt64(&g.facLastMin, atomic.SwapInt64(&g.facInMin, 0))
}
func (g *Guard) decrExp() {
g.lock.Lock()
if g.expPerMin > 0 {
g.expPerMin = g.expPerMin - 2
g.expThreshold = int64(float64(g.expPerMin) * _percentThreshold)
}
g.lock.Unlock()
}
func (g *Guard) incrFac() {
atomic.AddInt64(&g.facInMin, 1)
}
func (g *Guard) ok() (is bool) {
is = atomic.LoadInt64(&g.facLastMin) < atomic.LoadInt64(&g.expThreshold)
if is {
log.Warn("discovery is protected, the factual renews(%d) less than expected renews(%d)", atomic.LoadInt64(&g.facLastMin), atomic.LoadInt64(&g.expThreshold))
}
return
}

View File

@@ -0,0 +1,66 @@
package dao
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestIncrExp(t *testing.T) {
Convey("test IncrExp", t, func() {
re := new(Guard)
re.incrExp()
So(re.expPerMin, ShouldResemble, int64(2))
})
}
func TestDecrExp(t *testing.T) {
Convey("test DecrExp", t, func() {
re := new(Guard)
re.incrExp()
re.decrExp()
So(re.expPerMin, ShouldResemble, int64(0))
})
}
func TestSetExp(t *testing.T) {
Convey("test SetExp", t, func() {
re := new(Guard)
re.setExp(10)
So(re.expPerMin, ShouldResemble, int64(20))
So(re.expThreshold, ShouldResemble, int64(17))
})
}
func TestUpdateFac(t *testing.T) {
Convey("test UpdateFac", t, func() {
re := new(Guard)
re.incrFac()
re.updateFac()
So(re.facLastMin, ShouldResemble, int64(1))
})
}
func TestIncrFac(t *testing.T) {
Convey("test IncrFac", t, func() {
re := new(Guard)
re.incrFac()
So(re.facInMin, ShouldResemble, int64(1))
})
}
func TestIsProtected(t *testing.T) {
Convey("test IncrFac", t, func() {
re := new(Guard)
re.incrExp()
re.incrExp()
re.incrFac()
re.updateFac()
So(re.ok(), ShouldBeTrue)
re = new(Guard)
re.incrExp()
re.incrFac()
re.updateFac()
So(re.ok(), ShouldBeFalse)
})
}

View File

@@ -0,0 +1,179 @@
package dao
import (
"context"
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
"go-common/app/infra/discovery/conf"
"go-common/app/infra/discovery/model"
"go-common/library/ecode"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/xstr"
)
const (
_registerURL = "/discovery/register"
_cancelURL = "/discovery/cancel"
_renewURL = "/discovery/renew"
_setURL = "/discovery/set"
)
// Node represents a peer node to which information should be shared from this node.
//
// This struct handles replicating all update operations like 'Register,Renew,Cancel,Expiration and Status Changes'
// to the <Discovery Server> node it represents.
type Node struct {
c *conf.Config
client *bm.Client
pRegisterURL string
registerURL string
cancelURL string
renewURL string
setURL string
addr string
status model.NodeStatus
zone string
otherZone bool
}
// newNode return a node.
func newNode(c *conf.Config, addr string) (n *Node) {
n = &Node{
c: c,
addr: addr,
registerURL: fmt.Sprintf("http://%s%s", addr, _registerURL),
cancelURL: fmt.Sprintf("http://%s%s", addr, _cancelURL),
renewURL: fmt.Sprintf("http://%s%s", addr, _renewURL),
setURL: fmt.Sprintf("http://%s%s", addr, _setURL),
client: bm.NewClient(c.HTTPClient),
status: model.NodeStatusLost,
}
return
}
// Register send the registration information of Instance receiving by this node to the peer node represented.
func (n *Node) Register(c context.Context, i *model.Instance) (err error) {
err = n.call(c, model.Register, i, n.registerURL, nil)
if err != nil {
log.Warn("node be called(%s) register instance(%v) error(%v)", n.registerURL, i, err)
}
return
}
// Cancel send the cancellation information of Instance receiving by this node to the peer node represented.
func (n *Node) Cancel(c context.Context, i *model.Instance) (err error) {
err = n.call(c, model.Cancel, i, n.cancelURL, nil)
if ec := ecode.Cause(err); ec.Code() == ecode.NothingFound.Code() {
log.Warn("node be called(%s) instance(%v) already canceled", n.cancelURL, i)
}
return
}
// Renew send the heartbeat information of Instance receiving by this node to the peer node represented.
// If the instance does not exist the node, the instance registration information is sent again to the peer node.
func (n *Node) Renew(c context.Context, i *model.Instance) (err error) {
var res *model.Instance
err = n.call(c, model.Renew, i, n.renewURL, &res)
ec := ecode.Cause(err)
if ec.Code() == ecode.ServerErr.Code() {
log.Warn("node be called(%s) instance(%v) error(%v)", n.renewURL, i, err)
n.status = model.NodeStatusLost
return
}
n.status = model.NodeStatusUP
if ec.Code() == ecode.NothingFound.Code() {
log.Warn("node be called(%s) instance(%v) error(%v)", n.renewURL, i, err)
err = n.call(c, model.Register, i, n.registerURL, nil)
return
}
// NOTE: register response instance whitch in conflict with peer node
if ec.Code() == ecode.Conflict.Code() && res != nil {
err = n.call(c, model.Register, res, n.pRegisterURL, nil)
}
return
}
// Set the infomation of instance by this node to the peer node represented
func (n *Node) Set(c context.Context, arg *model.ArgSet) (err error) {
err = n.setCall(c, arg, n.setURL)
return
}
func (n *Node) call(c context.Context, action model.Action, i *model.Instance, uri string, data interface{}) (err error) {
params := url.Values{}
params.Set("region", i.Region)
params.Set("zone", i.Zone)
params.Set("env", i.Env)
params.Set("treeid", strconv.FormatInt(i.Treeid, 10))
params.Set("appid", i.Appid)
params.Set("hostname", i.Hostname)
if n.otherZone {
params.Set("replication", "false")
} else {
params.Set("replication", "true")
}
switch action {
case model.Register:
params.Set("status", strconv.FormatUint(uint64(i.Status), 10))
params.Set("version", i.Version)
meta, _ := json.Marshal(i.Metadata)
params.Set("metadata", string(meta))
params.Set("addrs", strings.Join(i.Addrs, ","))
params.Set("reg_timestamp", strconv.FormatInt(i.RegTimestamp, 10))
params.Set("dirty_timestamp", strconv.FormatInt(i.DirtyTimestamp, 10))
params.Set("latest_timestamp", strconv.FormatInt(i.LatestTimestamp, 10))
case model.Renew:
params.Set("dirty_timestamp", strconv.FormatInt(i.DirtyTimestamp, 10))
case model.Cancel:
params.Set("latest_timestamp", strconv.FormatInt(i.LatestTimestamp, 10))
}
var res struct {
Code int `json:"code"`
Data json.RawMessage `json:"data"`
}
if err = n.client.Post(c, uri, "", params, &res); err != nil {
log.Error("node be called(%s) instance(%v) error(%v)", uri, i, err)
return
}
if res.Code != 0 {
log.Error("node be called(%s) instance(%v) responce code(%v)", uri, i, res.Code)
if err = ecode.Int(res.Code); err == ecode.Conflict {
json.Unmarshal([]byte(res.Data), data)
}
return
}
return
}
func (n *Node) setCall(c context.Context, arg *model.ArgSet, uri string) (err error) {
params := url.Values{}
params.Set("region", arg.Region)
params.Set("zone", arg.Zone)
params.Set("env", arg.Env)
params.Set("appid", arg.Appid)
params.Set("hostname", strings.Join(arg.Hostname, ","))
params.Set("set_timestamp", strconv.FormatInt(arg.SetTimestamp, 10))
params.Set("replication", "true")
if len(arg.Status) != 0 {
params.Set("status", xstr.JoinInts(arg.Status))
}
if len(arg.Metadata) != 0 {
params.Set("metadata", strings.Join(arg.Metadata, ","))
}
var res struct {
Code int `json:"code"`
}
if err = n.client.Post(c, uri, "", params, &res); err != nil {
log.Error("node be setCalled(%s) appid(%s) env (%s) error(%v)", uri, arg.Appid, arg.Env, err)
return
}
if res.Code != 0 {
log.Error("node be setCalled(%s) appid(%s) env (%s) responce code(%v)", uri, arg.Appid, arg.Env, res.Code)
}
return
}

View File

@@ -0,0 +1,111 @@
package dao
import (
"context"
"strings"
"testing"
"time"
dc "go-common/app/infra/discovery/conf"
"go-common/app/infra/discovery/model"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/netutil/breaker"
xtime "go-common/library/time"
. "github.com/smartystreets/goconvey/convey"
gock "gopkg.in/h2non/gock.v1"
)
func TestCall(t *testing.T) {
Convey("test call", t, func() {
var res *model.Instance
node := newNode(&dc.Config{HTTPClient: &bm.ClientConfig{Breaker: &breaker.Config{Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100}, Timeout: xtime.Duration(time.Second), App: &bm.App{Key: "0c4b8fe3ff35a4b6", Secret: "b370880d1aca7d3a289b9b9a7f4d6812"}}, BM: &dc.HTTPServers{Inner: &bm.ServerConfig{Addr: "127.0.0.1:7171"}}, Nodes: []string{"127.0.0.1:7171"}}, "api.bilibili.co")
node.client.SetTransport(gock.DefaultTransport)
httpMock("POST", "http://api.bilibili.co/discovery/register").Reply(200).JSON(`{"ts":1514341945,"code":-409,"data":{"region":"shsb","zone":"fuck","appid":"main.arch.account-service","env":"pre","hostname":"cs4sq","http":"","rpc":"0.0.0.0:18888","weight":2}}`)
i := model.NewInstance(reg)
err := node.call(context.TODO(), model.Register, i, "http://api.bilibili.co/discovery/register", &res)
So(err, ShouldResemble, ecode.Conflict)
So(res.Appid, ShouldResemble, "main.arch.account-service")
})
}
func TestNodeCancel(t *testing.T) {
Convey("test node renew 409 error", t, func() {
i := model.NewInstance(reg)
node := newNode(&dc.Config{HTTPClient: &bm.ClientConfig{Breaker: &breaker.Config{Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100}, Timeout: xtime.Duration(time.Second), App: &bm.App{Key: "0c4b8fe3ff35a4b6", Secret: "b370880d1aca7d3a289b9b9a7f4d6812"}}, BM: &dc.HTTPServers{Inner: &bm.ServerConfig{Addr: "127.0.0.1:7171"}}, Nodes: []string{"127.0.0.1:7171"}}, "api.bilibili.co")
node.pRegisterURL = "http://127.0.0.1:7171/discovery/register"
node.client.SetTransport(gock.DefaultTransport)
httpMock("POST", "http://api.bilibili.co/discovery/cancel").Reply(200).JSON(`{"code":0}`)
err := node.Cancel(context.TODO(), i)
So(err, ShouldBeNil)
})
}
func TestNodeRenew(t *testing.T) {
Convey("test node renew 409 error", t, func() {
i := model.NewInstance(reg)
node := newNode(&dc.Config{HTTPClient: &bm.ClientConfig{Breaker: &breaker.Config{Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100}, Timeout: xtime.Duration(time.Second), App: &bm.App{Key: "0c4b8fe3ff35a4b6", Secret: "b370880d1aca7d3a289b9b9a7f4d6812"}}, BM: &dc.HTTPServers{Inner: &bm.ServerConfig{Addr: "127.0.0.1:7171"}}, Nodes: []string{"127.0.0.1:7171"}}, "api.bilibili.co")
node.pRegisterURL = "http://127.0.0.1:7171/discovery/register"
node.client.SetTransport(gock.DefaultTransport)
httpMock("POST", "http://api.bilibili.co/discovery/renew").Reply(200).JSON(`{"code":-409,"data":{"region":"shsb","zone":"fuck","appid":"main.arch.account-service","env":"pre","hostname":"cs4sq","http":"","rpc":"0.0.0.0:18888","weight":2}}`)
httpMock("POST", "http://127.0.0.1:7171/discovery/register").Reply(200).JSON(`{"code":0}`)
err := node.Renew(context.TODO(), i)
So(err, ShouldBeNil)
})
}
func TestNodeRenew2(t *testing.T) {
Convey("test node renew 404 error", t, func() {
i := model.NewInstance(reg)
node := newNode(&dc.Config{HTTPClient: &bm.ClientConfig{Breaker: &breaker.Config{Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100}, Timeout: xtime.Duration(time.Second), App: &bm.App{Key: "0c4b8fe3ff35a4b6", Secret: "b370880d1aca7d3a289b9b9a7f4d6812"}}, BM: &dc.HTTPServers{Inner: &bm.ServerConfig{Addr: "127.0.0.1:7171"}}, Nodes: []string{"127.0.0.1:7171"}}, "api.bilibili.co")
node.client.SetTransport(gock.DefaultTransport)
httpMock("POST", "http://api.bilibili.co/discovery/renew").Reply(200).JSON(`{"code":-404}`)
httpMock("POST", "http://api.bilibili.co/discovery/register").Reply(200).JSON(`{"code":0}`)
err := node.Renew(context.TODO(), i)
So(err, ShouldBeNil)
})
}
func TestSet(t *testing.T) {
Convey("test set", t, func() {
node := newNode(&dc.Config{HTTPClient: &bm.ClientConfig{Breaker: &breaker.Config{Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100}, Timeout: xtime.Duration(time.Second), App: &bm.App{Key: "0c4b8fe3ff35a4b6", Secret: "b370880d1aca7d3a289b9b9a7f4d6812"}}, BM: &dc.HTTPServers{Inner: &bm.ServerConfig{Addr: "127.0.0.1:7171"}}, Nodes: []string{"127.0.0.1:7171"}}, "api.bilibili.co")
node.client.SetTransport(gock.DefaultTransport)
httpMock("POST", "http://api.bilibili.co/discovery/set").Reply(200).JSON(`{"ts":1514341945,"code":0}`)
set := &model.ArgSet{
Region: "shsb",
Env: "pre",
Appid: "main.arch.account-service",
Hostname: []string{"test1"},
Status: []int64{1},
}
err := node.Set(context.TODO(), set)
So(err, ShouldBeNil)
})
}
func httpMock(method, url string) *gock.Request {
r := gock.New(url)
r.Method = strings.ToUpper(method)
return r
}

View File

@@ -0,0 +1,170 @@
package dao
import (
"context"
"fmt"
"math/rand"
"go-common/app/infra/discovery/conf"
"go-common/app/infra/discovery/model"
"go-common/library/sync/errgroup"
)
// Nodes is helper to manage lifecycle of a collection of Nodes.
type Nodes struct {
nodes []*Node
zones map[string][]*Node
selfAddr string
}
// NewNodes new nodes and return.
func NewNodes(c *conf.Config) *Nodes {
nodes := make([]*Node, 0, len(c.Nodes))
for _, addr := range c.Nodes {
n := newNode(c, addr)
n.pRegisterURL = fmt.Sprintf("http://%s%s", c.BM.Inner.Addr, _registerURL)
nodes = append(nodes, n)
}
zones := make(map[string][]*Node)
for name, addrs := range c.Zones {
var znodes []*Node
for _, addr := range addrs {
n := newNode(c, addr)
n.otherZone = true
n.zone = name
n.pRegisterURL = fmt.Sprintf("http://%s%s", c.BM.Inner.Addr, _registerURL)
znodes = append(znodes, n)
}
zones[name] = znodes
}
return &Nodes{
nodes: nodes,
zones: zones,
selfAddr: c.BM.Inner.Addr,
}
}
// Replicate replicate information to all nodes except for this node.
func (ns *Nodes) Replicate(c context.Context, action model.Action, i *model.Instance, otherZone bool) (err error) {
if len(ns.nodes) == 0 {
return
}
eg, c := errgroup.WithContext(c)
for _, n := range ns.nodes {
if !ns.Myself(n.addr) {
ns.action(c, eg, action, n, i)
}
}
if !otherZone {
for _, zns := range ns.zones {
if n := len(zns); n > 0 {
ns.action(c, eg, action, zns[rand.Intn(n)], i)
}
}
}
err = eg.Wait()
return
}
func (ns *Nodes) action(c context.Context, eg *errgroup.Group, action model.Action, n *Node, i *model.Instance) {
switch action {
case model.Register:
eg.Go(func() error {
n.Register(c, i)
return nil
})
case model.Renew:
eg.Go(func() error {
n.Renew(c, i)
return nil
})
case model.Cancel:
eg.Go(func() error {
n.Cancel(c, i)
return nil
})
}
}
// ReplicateSet replicate set information to all nodes except for this node.
func (ns *Nodes) ReplicateSet(c context.Context, arg *model.ArgSet, otherZone bool) (err error) {
if len(ns.nodes) == 0 {
return
}
eg, c := errgroup.WithContext(c)
for _, n := range ns.nodes {
if !ns.Myself(n.addr) {
eg.Go(func() error {
return n.Set(c, arg)
})
}
}
if !otherZone {
for _, zns := range ns.zones {
if n := len(zns); n > 0 {
node := zns[rand.Intn(n)]
eg.Go(func() error {
return node.Set(c, arg)
})
}
}
}
err = eg.Wait()
return
}
// Nodes returns nodes of local zone.
func (ns *Nodes) Nodes() (nsi []*model.Node) {
nsi = make([]*model.Node, 0, len(ns.nodes))
for _, nd := range ns.nodes {
if nd.otherZone {
continue
}
node := &model.Node{
Addr: nd.addr,
Status: nd.status,
Zone: nd.zone,
}
nsi = append(nsi, node)
}
return
}
// AllNodes returns nodes contain other zone nodes.
func (ns *Nodes) AllNodes() (nsi []*model.Node) {
nsi = make([]*model.Node, 0, len(ns.nodes))
for _, nd := range ns.nodes {
node := &model.Node{
Addr: nd.addr,
Status: nd.status,
Zone: nd.zone,
}
nsi = append(nsi, node)
}
for _, zns := range ns.zones {
if n := len(zns); n > 0 {
nd := zns[rand.Intn(n)]
node := &model.Node{
Addr: nd.addr,
Status: nd.status,
Zone: nd.zone,
}
nsi = append(nsi, node)
}
}
return
}
// Myself returns whether or not myself.
func (ns *Nodes) Myself(addr string) bool {
return ns.selfAddr == addr
}
// UP marks status of myself node up.
func (ns *Nodes) UP() {
for _, nd := range ns.nodes {
if ns.Myself(nd.addr) {
nd.status = model.NodeStatusUP
}
}
}

View File

@@ -0,0 +1,82 @@
package dao
import (
"context"
"testing"
"time"
dc "go-common/app/infra/discovery/conf"
"go-common/app/infra/discovery/model"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/netutil/breaker"
xtime "go-common/library/time"
. "github.com/smartystreets/goconvey/convey"
gock "gopkg.in/h2non/gock.v1"
)
func TestReplicate(t *testing.T) {
Convey("test replicate", t, func() {
i := model.NewInstance(reg)
nodes := NewNodes(&dc.Config{HTTPClient: &bm.ClientConfig{Breaker: &breaker.Config{Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100}, Timeout: xtime.Duration(time.Second), App: &bm.App{Key: "0c4b8fe3ff35a4b6", Secret: "b370880d1aca7d3a289b9b9a7f4d6812"}}, BM: &dc.HTTPServers{Inner: &bm.ServerConfig{Addr: "127.0.0.1:7171"}}, Nodes: []string{"api.bilibili.co", "uat-bilibili.co", "127.0.0.1:7171"}})
nodes.nodes[0].client.SetTransport(gock.DefaultTransport)
nodes.nodes[1].client.SetTransport(gock.DefaultTransport)
httpMock("POST", "http://api.bilibili.co/discovery/register").Reply(200).JSON(`{"code":0}`)
httpMock("POST", "http://uat-bilibili.co/discovery/register").Reply(200).JSON(`{"code":0}`)
err := nodes.Replicate(context.TODO(), model.Register, i, false)
So(err, ShouldBeNil)
})
}
func TestReplicateSet(t *testing.T) {
Convey("test replicate set", t, func() {
nodes := NewNodes(&dc.Config{HTTPClient: &bm.ClientConfig{Breaker: &breaker.Config{Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100}, Timeout: xtime.Duration(time.Second), App: &bm.App{Key: "0c4b8fe3ff35a4b6", Secret: "b370880d1aca7d3a289b9b9a7f4d6812"}}, BM: &dc.HTTPServers{Inner: &bm.ServerConfig{Addr: "127.0.0.1:7171"}}, Nodes: []string{"api.bilibili.co"}})
nodes.nodes[0].client.SetTransport(gock.DefaultTransport)
httpMock("POST", "http://api.bilibili.co/discovery/set").Reply(200).JSON(`{"code":0}`)
set := &model.ArgSet{
Region: "shsb",
Env: "pre",
Appid: "main.arch.account-service",
Hostname: []string{"test1"},
Status: []int64{1},
}
err := nodes.ReplicateSet(context.TODO(), set, false)
So(err, ShouldBeNil)
})
}
func TestNodes(t *testing.T) {
Convey("test replicate set", t, func() {
nodes := NewNodes(&dc.Config{HTTPClient: &bm.ClientConfig{Breaker: &breaker.Config{Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100}, Timeout: xtime.Duration(time.Second), App: &bm.App{Key: "0c4b8fe3ff35a4b6", Secret: "b370880d1aca7d3a289b9b9a7f4d6812"}}, BM: &dc.HTTPServers{Inner: &bm.ServerConfig{Addr: "127.0.0.1:7171"}}, Nodes: []string{"api.bilibili.co", "uat-bilibili.co", "127.0.0.1:7171"}})
res := nodes.Nodes()
So(len(res), ShouldResemble, 3)
})
}
func TestUp(t *testing.T) {
Convey("test up", t, func() {
nodes := NewNodes(&dc.Config{HTTPClient: &bm.ClientConfig{Breaker: &breaker.Config{Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100}, Timeout: xtime.Duration(time.Second), App: &bm.App{Key: "0c4b8fe3ff35a4b6", Secret: "b370880d1aca7d3a289b9b9a7f4d6812"}}, BM: &dc.HTTPServers{Inner: &bm.ServerConfig{Addr: "127.0.0.1:7171"}}, Nodes: []string{"api.bilibili.co", "uat-bilibili.co", "127.0.0.1:7171"}})
nodes.UP()
for _, nd := range nodes.nodes {
if nd.addr == "127.0.0.1:7171" {
So(nd.status, ShouldResemble, model.NodeStatusUP)
}
}
})
}

View File

@@ -0,0 +1,387 @@
package dao
import (
"context"
"fmt"
"math/rand"
"strconv"
"sync"
"time"
"go-common/app/infra/discovery/model"
"go-common/library/ecode"
"go-common/library/log"
)
const (
_evictThreshold = int64(90 * time.Second)
_evictCeiling = int64(3600 * time.Second)
)
// Registry handles replication of all operations to peer Discovery nodes to keep them all in sync.
type Registry struct {
appm map[string]*model.Apps // appid-env -> apps
aLock sync.RWMutex
conns map[string]map[string]*conn // zone.env.appid-> host
cLock sync.RWMutex
gd *Guard
}
// conn the poll chan contains consumer.
type conn struct {
ch chan map[string]*model.InstanceInfo // TODO(felix): increase
arg *model.ArgPolls
latestTime int64
count int
}
// newConn new consumer chan.
func newConn(ch chan map[string]*model.InstanceInfo, latestTime int64, arg *model.ArgPolls) *conn {
return &conn{ch: ch, latestTime: latestTime, arg: arg, count: 1}
}
// NewRegistry new register.
func NewRegistry() (r *Registry) {
r = &Registry{
appm: make(map[string]*model.Apps),
conns: make(map[string]map[string]*conn),
gd: new(Guard),
}
go r.proc()
return
}
func (r *Registry) newapps(appid, env string) (a *model.Apps, ok bool) {
key := appsKey(appid, env)
r.aLock.Lock()
if a, ok = r.appm[key]; !ok {
a = model.NewApps()
r.appm[key] = a
}
r.aLock.Unlock()
return
}
func (r *Registry) apps(appid, env, zone string) (as []*model.App, a *model.Apps, ok bool) {
key := appsKey(appid, env)
r.aLock.RLock()
a, ok = r.appm[key]
r.aLock.RUnlock()
if ok {
as = a.App(zone)
}
return
}
func appsKey(appid, env string) string {
// NOTE disocvery 不区分具体环境
if appid == model.AppID {
return appid
}
return fmt.Sprintf("%s-%s", appid, env)
}
func (r *Registry) newApp(ins *model.Instance) (a *model.App) {
as, _ := r.newapps(ins.Appid, ins.Env)
a, _ = as.NewApp(ins.Zone, ins.Appid, ins.Treeid, ins.LatestTimestamp)
return
}
// Register a new instance.
func (r *Registry) Register(ins *model.Instance, latestTime int64) (err error) {
a := r.newApp(ins)
i, ok := a.NewInstance(ins, latestTime)
if ok {
r.gd.incrExp()
}
// NOTE: make sure free poll before update appid latest timestamp.
r.broadcast(i.Env, i.Appid, a)
return
}
// Renew marks the given instance of the given app name as renewed, and also marks whether it originated from replication.
func (r *Registry) Renew(arg *model.ArgRenew) (i *model.Instance, ok bool) {
a, _, _ := r.apps(arg.Appid, arg.Env, arg.Zone)
if len(a) == 0 {
return
}
if i, ok = a[0].Renew(arg.Hostname); !ok {
return
}
r.gd.incrFac()
return
}
// Cancel cancels the registration of an instance.
func (r *Registry) Cancel(arg *model.ArgCancel) (i *model.Instance, ok bool) {
if i, ok = r.cancel(arg.Zone, arg.Env, arg.Appid, arg.Hostname, arg.LatestTimestamp); !ok {
return
}
r.gd.decrExp()
return
}
func (r *Registry) cancel(zone, env, appid, hostname string, latestTime int64) (i *model.Instance, ok bool) {
var l int
a, as, _ := r.apps(appid, env, zone)
if len(a) == 0 {
return
}
if i, l, ok = a[0].Cancel(hostname, latestTime); !ok {
return
}
as.UpdateLatest(latestTime)
if l == 0 {
if a[0].Len() == 0 {
as.Del(zone)
}
}
if len(as.App("")) == 0 {
r.aLock.Lock()
delete(r.appm, appsKey(appid, env))
r.aLock.Unlock()
}
r.broadcast(env, appid, a[0]) // NOTE: make sure free poll before update appid latest timestamp.
return
}
// FetchAll fetch all instances of all the families.
func (r *Registry) FetchAll() (im map[string][]*model.Instance) {
ass := r.allapp()
im = make(map[string][]*model.Instance)
for _, as := range ass {
for _, a := range as.App("") {
im[a.AppID] = append(im[a.AppID], a.Instances()...)
}
}
return
}
// Fetch fetch all instances by appid.
func (r *Registry) Fetch(zone, env, appid string, latestTime int64, status uint32) (info *model.InstanceInfo, err error) {
key := appsKey(appid, env)
r.aLock.RLock()
a, ok := r.appm[key]
r.aLock.RUnlock()
if !ok {
err = ecode.NothingFound
return
}
info, err = a.InstanceInfo(zone, latestTime, status)
return
}
// Polls hangs request and then write instances when that has changes, or return NotModified.
func (r *Registry) Polls(arg *model.ArgPolls) (ch chan map[string]*model.InstanceInfo, new bool, err error) {
var (
ins = make(map[string]*model.InstanceInfo, len(arg.Treeid))
in *model.InstanceInfo
)
if len(arg.Appid) != len(arg.LatestTimestamp) {
arg.LatestTimestamp = make([]int64, len(arg.Appid))
}
for i := range arg.Appid {
in, err = r.Fetch(arg.Zone, arg.Env, arg.Appid[i], arg.LatestTimestamp[i], model.InstanceStatusUP)
if err == ecode.NothingFound {
log.Error("Polls region(%s) zone(%s) env(%s) appid(%s) error(%v)", arg.Region, arg.Zone, arg.Env, arg.Appid[i], err)
return
}
if err == nil {
if len(arg.Treeid) != 0 {
ins[strconv.FormatInt(arg.Treeid[i], 10)] = in
} else {
ins[arg.Appid[i]] = in
}
new = true
}
}
if new {
ch = make(chan map[string]*model.InstanceInfo, 1)
ch <- ins
return
}
r.cLock.Lock()
for i := range arg.Appid {
k := appsKey(arg.Appid[i], arg.Env)
if _, ok := r.conns[k]; !ok {
r.conns[k] = make(map[string]*conn, 1)
}
connection, ok := r.conns[k][arg.Hostname]
if !ok {
if ch == nil {
ch = make(chan map[string]*model.InstanceInfo, 5) // NOTE: there maybe have more than one connection on the same hostname!!!
}
connection = newConn(ch, arg.LatestTimestamp[i], arg)
log.Info("Polls from(%s) new connection(%d)", arg.Hostname, connection.count)
} else {
connection.count++ // NOTE: there maybe have more than one connection on the same hostname!!!
if ch == nil {
ch = connection.ch
}
log.Info("Polls from(%s) reuse connection(%d)", arg.Hostname, connection.count)
}
r.conns[k][arg.Hostname] = connection
}
r.cLock.Unlock()
return
}
// Polling get polling clients.
func (r *Registry) Polling(arg *model.ArgPolling) (resp []string, err error) {
r.cLock.RLock()
conns, ok := r.conns[appsKey(arg.Appid, arg.Env)]
if !ok {
r.cLock.RUnlock()
return
}
resp = make([]string, 0, len(conns))
for host := range conns {
resp = append(resp, host)
}
r.cLock.RUnlock()
return
}
// broadcast on poll by chan.
// NOTE: make sure free poll before update appid latest timestamp.
func (r *Registry) broadcast(env, appid string, a *model.App) {
key := appsKey(appid, env)
r.cLock.Lock()
defer r.cLock.Unlock()
conns, ok := r.conns[key]
if !ok {
return
}
delete(r.conns, key)
for _, conn := range conns {
ii, _ := r.Fetch(conn.arg.Zone, env, appid, 0, model.InstanceStatusUP) // TODO(felix): latesttime!=0 increase
var key string
if len(conn.arg.Treeid) != 0 {
key = strconv.FormatInt(a.Treeid, 10)
} else {
key = a.AppID
}
for i := 0; i < conn.count; i++ { // NOTE: there maybe have more than one connection on the same hostname!!!
select {
case conn.ch <- map[string]*model.InstanceInfo{key: ii}: // NOTE: if chan is full, means no poller.
log.Info("broadcast to(%s) success(%d)", conn.arg.Hostname, i+1)
case <-time.After(time.Millisecond * 500):
log.Info("broadcast to(%s) failed(%d) maybe chan full", conn.arg.Hostname, i+1)
}
}
}
}
// Set Set the status of instance by hostnames.
func (r *Registry) Set(c context.Context, arg *model.ArgSet) (ok bool) {
a, _, _ := r.apps(arg.Appid, arg.Env, arg.Zone)
if len(a) == 0 {
return
}
if ok = a[0].Set(arg); !ok {
return
}
r.broadcast(arg.Env, arg.Appid, a[0])
return
}
func (r *Registry) allapp() (ass []*model.Apps) {
r.aLock.RLock()
ass = make([]*model.Apps, 0, len(r.appm))
for _, as := range r.appm {
ass = append(ass, as)
}
r.aLock.RUnlock()
return
}
// reset expect renews, count the renew of all app, one app has two expect remews in minute.
func (r *Registry) resetExp() {
cnt := int64(0)
for _, p := range r.allapp() {
for _, a := range p.App("") {
cnt += int64(a.Len())
}
}
r.gd.setExp(cnt)
}
func (r *Registry) proc() {
tk := time.Tick(1 * time.Minute)
tk2 := time.Tick(15 * time.Minute)
for {
select {
case <-tk:
r.gd.updateFac()
r.evict()
case <-tk2:
r.resetExp()
}
}
}
func (r *Registry) evict() {
protect := r.gd.ok()
// We collect first all expired items, to evict them in random order. For large eviction sets,
// if we do not that, we might wipe out whole apps before self preservation kicks in. By randomizing it,
// the impact should be evenly distributed across all applications.
var eis []*model.Instance
var registrySize int
// all projects
ass := r.allapp()
for _, as := range ass {
for _, a := range as.App("") {
registrySize += a.Len()
is := a.Instances()
for _, i := range is {
delta := time.Now().UnixNano() - i.RenewTimestamp
if (!protect && delta > _evictThreshold) || delta > _evictCeiling {
eis = append(eis, i)
}
}
}
}
// To compensate for GC pauses or drifting local time, we need to use current registry size as a base for
// triggering self-preservation. Without that we would wipe out full registry.
eCnt := len(eis)
registrySizeThreshold := int(float64(registrySize) * _percentThreshold)
evictionLimit := registrySize - registrySizeThreshold
if eCnt > evictionLimit {
eCnt = evictionLimit
}
if eCnt == 0 {
return
}
for i := 0; i < eCnt; i++ {
// Pick a random item (Knuth shuffle algorithm)
next := i + rand.Intn(len(eis)-i)
eis[i], eis[next] = eis[next], eis[i]
ei := eis[i]
r.cancel(ei.Zone, ei.Env, ei.Appid, ei.Hostname, time.Now().UnixNano())
}
}
// DelConns delete conn of host in appid
func (r *Registry) DelConns(arg *model.ArgPolls) {
r.cLock.Lock()
for i := range arg.Appid {
k := appsKey(arg.Appid[i], arg.Env)
conns, ok := r.conns[k]
if !ok {
log.Warn("DelConn key(%s) not found", k)
continue
}
if connection, ok := conns[arg.Hostname]; ok {
if connection.count > 1 {
log.Info("DelConns from(%s) count decr(%d)", arg.Hostname, connection.count)
connection.count--
} else {
log.Info("DelConns from(%s) delete(%d)", arg.Hostname, connection.count)
delete(conns, arg.Hostname)
}
}
}
r.cLock.Unlock()
}

View File

@@ -0,0 +1,454 @@
package dao
import (
"context"
"encoding/json"
"fmt"
"sync"
"testing"
"time"
"go-common/app/infra/discovery/model"
"go-common/library/ecode"
. "github.com/smartystreets/goconvey/convey"
)
var reg = &model.ArgRegister{Appid: "main.arch.test", Hostname: "reg", RPC: "127.0.0.1:8080", Region: "shsb", Zone: "sh0001", Env: "pre", Status: 1}
var regH1 = &model.ArgRegister{Appid: "main.arch.test", Hostname: "regH1", RPC: "127.0.0.1:8080", Region: "shsb", Zone: "sh0001", Env: "pre", Status: 1}
var reg2 = &model.ArgRegister{Appid: "main.arch.test2", Hostname: "reg2", RPC: "127.0.0.1:8080", Region: "shsb", Zone: "sh0001", Env: "pre", Status: 1}
var arg = &model.ArgRenew{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: "main.arch.test", Hostname: "reg"}
var cancel = &model.ArgCancel{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: "main.arch.test", Hostname: "reg"}
var cancel2 = &model.ArgCancel{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: "main.arch.test", Hostname: "regH1"}
func TestReigster(t *testing.T) {
i := model.NewInstance(reg)
register(t, i)
}
func TestDiscovery(t *testing.T) {
i1 := model.NewInstance(reg)
i2 := model.NewInstance(regH1)
fmt.Println(_evictThreshold)
r := register(t, i1, i2)
Convey("test discovery", t, func() {
pollArg := &model.ArgPolls{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: []string{"main.arch.test"}, Hostname: "test"}
fetchArg := &model.ArgFetch{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: "main.arch.test", Status: 3}
info, err := r.Fetch(fetchArg.Zone, fetchArg.Env, fetchArg.Appid, 0, fetchArg.Status)
So(err, ShouldBeNil)
So(len(info.Instances), ShouldEqual, 2)
ch, _, err := r.Polls(pollArg)
So(err, ShouldBeNil)
apps := <-ch
So(len(apps["main.arch.test"].Instances), ShouldEqual, 2)
pollArg.LatestTimestamp[0] = apps["main.arch.test"].LatestTimestamp
fmt.Println(apps["main.arch.test"])
r.Cancel(cancel)
ch, _, err = r.Polls(pollArg)
So(err, ShouldBeNil)
apps = <-ch
So(len(apps["main.arch.test"].Instances), ShouldEqual, 1)
pollArg.LatestTimestamp[0] = apps["main.arch.test"].LatestTimestamp
r.Cancel(cancel2)
})
}
func TestRenew(t *testing.T) {
src := model.NewInstance(reg)
r := register(t, src)
Convey("test renew", t, func() {
i, ok := r.Renew(arg)
So(ok, ShouldBeTrue)
So(i, ShouldResemble, src)
})
}
func BenchmarkRenew(b *testing.B) {
var (
i *model.Instance
ok bool
)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
r, src := benchRegister(b)
if i, ok = r.Renew(arg); !ok {
b.Errorf("Renew(%v)", src.Appid)
}
benchCompareInstance(b, src, i)
}
})
}
func TestCancel(t *testing.T) {
src := model.NewInstance(reg)
r := register(t, src)
Convey("test cancel", t, func() {
i, ok := r.Cancel(cancel)
So(ok, ShouldBeTrue)
So(i, ShouldResemble, src)
fetchArg := &model.ArgFetch{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: "main.arch.test", Status: 3}
_, err := r.Fetch(fetchArg.Zone, fetchArg.Env, fetchArg.Appid, 0, fetchArg.Status)
So(err, ShouldResemble, ecode.NothingFound)
})
}
func BenchmarkCancel(b *testing.B) {
var (
i *model.Instance
ok bool
err error
)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
r, src := benchRegister(b)
if i, ok = r.Cancel(cancel); !ok {
b.Errorf("Cancel(%v) error", src.Appid)
}
benchCompareInstance(b, src, i)
fetchArg := &model.ArgFetch{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: "main.arch.test", Status: 3}
if _, err = r.Fetch(fetchArg.Zone, fetchArg.Env, fetchArg.Appid, 0, fetchArg.Status); err != ecode.NothingFound {
b.Errorf("Fetch(%v) error(%v)", src.Appid, err)
}
}
})
}
func TestFetchAll(t *testing.T) {
i := model.NewInstance(reg)
r := register(t, i)
Convey("test fetch all", t, func() {
am := r.FetchAll()
So(len(am), ShouldResemble, 1)
})
}
func BenchmarkFetchAll(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
r, _ := benchRegister(b)
if am := r.FetchAll(); len(am) != 1 {
b.Errorf("FetchAll() error")
}
}
})
}
func TestFetch(t *testing.T) {
i := model.NewInstance(reg)
r := register(t, i)
Convey("test fetch", t, func() {
fetchArg2 := &model.ArgFetch{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: "main.arch.test", Status: 1}
c, err := r.Fetch(fetchArg2.Zone, fetchArg2.Env, fetchArg2.Appid, 0, fetchArg2.Status)
So(err, ShouldBeNil)
So(len(c.Instances), ShouldResemble, 1)
})
}
func BenchmarkFetch(b *testing.B) {
var (
err error
c *model.InstanceInfo
)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
r, _ := benchRegister(b)
fetchArg := &model.ArgFetch{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: "main.arch.test", Status: 1}
if c, err = r.Fetch(fetchArg.Zone, fetchArg.Env, fetchArg.Appid, 0, fetchArg.Status); err != nil {
b.Errorf("Fetch(%v) error(%v)", arg.Appid, err)
}
fetchArg2 := &model.ArgFetch{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: "main.arch.test", Status: 2}
if c, err = r.Fetch(fetchArg2.Zone, fetchArg2.Env, fetchArg2.Appid, 0, fetchArg2.Status); err != nil {
b.Errorf("Fetch(%v) error(%v)", arg.Appid, err)
}
_ = c
}
})
}
func TestPoll(t *testing.T) {
i := model.NewInstance(reg)
r := register(t, i)
Convey("test poll", t, func() {
pollArg := &model.ArgPolls{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: []string{"main.arch.test"}, Hostname: "csq"}
ch, _, err := r.Polls(pollArg)
So(err, ShouldBeNil)
c := <-ch
So(len(c[pollArg.Appid[0]].Instances), ShouldEqual, 1)
})
}
func TestPolls(t *testing.T) {
i1 := model.NewInstance(reg)
i2 := model.NewInstance(reg2)
r := register(t, i1, i2)
Convey("test polls", t, func() {
pollArg := &model.ArgPolls{Region: "shsb", Zone: "sh0001", Env: "pre", LatestTimestamp: []int64{0, 0}, Appid: []string{"main.arch.test", "main.arch.test2"}, Hostname: "csq"}
ch, new, err := r.Polls(pollArg)
So(err, ShouldBeNil)
So(new, ShouldBeTrue)
c := <-ch
So(len(c), ShouldResemble, 2)
})
}
func TestPollsParallel(t *testing.T) {
i1 := model.NewInstance(reg)
i2 := model.NewInstance(reg2)
r := register(t, i1, i2)
Convey("test polls parallel", t, func(c C) {
var (
wg sync.WaitGroup
ch1, ch2 chan map[string]*model.InstanceInfo
new bool
err error
)
pollArg := &model.ArgPolls{Region: "shsb", Zone: "sh0001", Env: "pre", LatestTimestamp: []int64{time.Now().UnixNano(), time.Now().UnixNano()}, Appid: []string{"main.arch.test", "main.arch.test2"}, Hostname: "csq"}
ch1, new, err = r.Polls(pollArg)
c.So(err, ShouldEqual, ecode.NotModified)
c.So(new, ShouldBeFalse)
c.So(ch1, ShouldNotBeNil)
ch2, new, err = r.Polls(pollArg)
c.So(err, ShouldEqual, ecode.NotModified)
c.So(new, ShouldBeFalse)
c.So(ch2, ShouldNotBeNil)
// wait group
wg.Add(2)
go func() {
res := <-ch1
c.So(len(res), ShouldResemble, 1)
ress, _ := json.Marshal(res)
fmt.Println("chenggongle 1!!!", string(ress))
wg.Done()
}()
go func() {
res := <-ch2
c.So(len(res), ShouldResemble, 1)
ress, _ := json.Marshal(res)
fmt.Println("chenggongle 2!!!", string(ress))
wg.Done()
}()
// re register when 1s later, make sure latest_timestamp changed
time.Sleep(time.Second)
h1 := model.NewInstance(regH1)
r.Register(h1, 0)
// wait
wg.Wait()
})
}
func BenchmarkPoll(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
var (
err error
ch chan map[string]*model.InstanceInfo
c map[string]*model.InstanceInfo
)
r, _ := benchRegister(b)
pollArg := &model.ArgPolls{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: []string{"main.arch.test"}, Hostname: "csq"}
if ch, _, err = r.Polls(pollArg); err != nil {
b.Errorf("Poll(%v) error(%v)", arg.Appid, err)
}
if c = <-ch; len(c[pollArg.Appid[0]].Instances) != 1 {
b.Errorf("Poll(%v) lenth error", arg.Appid)
}
}
})
}
func TestBroadcast(t *testing.T) {
i := model.NewInstance(reg)
r := register(t, i)
Convey("test poll push connection", t, func() {
go func() {
Convey("must poll ahead of time", t, func() {
time.Sleep(time.Microsecond * 5)
var arg2 = &model.ArgRegister{Appid: "main.arch.test", Hostname: "go", RPC: "127.0.0.1:8080", Region: "shsb", Zone: "sh0001", Env: "pre", Status: 1}
m2 := model.NewInstance(arg2)
err2 := r.Register(m2, 0)
So(err2, ShouldBeNil)
})
}()
pollArg := &model.ArgPolls{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: []string{"main.arch.test"}, LatestTimestamp: []int64{time.Now().UnixNano()}}
ch, _, err := r.Polls(pollArg)
So(err, ShouldResemble, ecode.NotModified)
c := <-ch
So(len(c[pollArg.Appid[0]].Instances), ShouldResemble, 2)
So(c[pollArg.Appid[0]].ZoneInstances, ShouldNotBeNil)
So(len(c[pollArg.Appid[0]].ZoneInstances["sh0001"]), ShouldResemble, 2)
})
}
func BenchmarkBroadcast(b *testing.B) {
for i := 0; i < b.N; i++ {
var (
err error
err2 error
ch chan map[string]*model.InstanceInfo
c map[string]*model.InstanceInfo
)
r, _ := benchRegister(b)
go func() {
time.Sleep(time.Millisecond * 1)
var arg2 = &model.ArgRegister{Appid: "main.arch.test", Hostname: "go", RPC: "127.0.0.1:8080", Region: "shsb", Zone: "sh0001", Env: "pre", Status: 1}
m2 := model.NewInstance(arg2)
if err2 = r.Register(m2, 0); err2 != nil {
b.Errorf("Reigster(%v) error(%v)", m2.Appid, err2)
}
}()
pollArg := &model.ArgPolls{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: []string{"main.arch.test"}, LatestTimestamp: []int64{time.Now().UnixNano()}}
if ch, _, err = r.Polls(pollArg); err != nil && err != ecode.NotModified {
b.Errorf("Poll(%v) error(%v)", pollArg.Appid, err)
}
c = <-ch
if len(c[pollArg.Appid[0]].Instances) != 2 {
b.Errorf("Poll(%v) length error", pollArg.Appid)
}
if c[pollArg.Appid[0]].ZoneInstances == nil {
b.Errorf("Poll(%v) zone instances nil error", pollArg.Appid)
}
if len(c[pollArg.Appid[0]].ZoneInstances["sh0001"]) != 2 {
b.Errorf("Poll(%v) zone instances length error", pollArg.Appid)
}
}
}
func TestRegistrySet(t *testing.T) {
i := model.NewInstance(reg)
r := register(t, i)
changes := make(map[string]string)
changes["reg"] = "1"
Convey("test set weight to 1", t, func() {
set := &model.ArgSet{
Region: "shsb",
Env: "pre",
Appid: "main.arch.test",
Hostname: []string{"reg"},
Metadata: []string{`{"weight":"1"}`},
}
ok := r.Set(context.TODO(), set)
So(ok, ShouldBeTrue)
fetchArg := &model.ArgFetch{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: "main.arch.test", Status: 3}
c, err := r.Fetch(fetchArg.Zone, fetchArg.Env, fetchArg.Appid, 0, fetchArg.Status)
So(err, ShouldBeNil)
So(c.Instances[0].Metadata["weight"], ShouldResemble, "1")
})
}
func BenchmarkSet(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
var (
c *model.InstanceInfo
err error
ok bool
)
r, _ := benchRegister(b)
set := &model.ArgSet{
Region: "shsb",
Env: "pre",
Appid: "main.arch.account-service",
Hostname: []string{"test1"},
Status: []int64{1},
Metadata: []string{`{"weight":"1"}`},
}
if ok = r.Set(context.TODO(), set); !ok {
b.Errorf("SetWeight(%v) error", arg.Appid)
}
fetchArg := &model.ArgFetch{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: "main.arch.test", Status: 3}
if c, err = r.Fetch(fetchArg.Zone, fetchArg.Env, fetchArg.Appid, 0, fetchArg.Status); err != nil {
b.Errorf("Fetch(%v) error(%v)", fetchArg.Appid, err)
}
if c.Instances[0].Metadata["weight"] != "1" {
b.Errorf("SetWeight(%v) change error", fetchArg.Appid)
}
}
})
}
func TestResetExp(t *testing.T) {
i := model.NewInstance(reg)
r := register(t, i)
Convey("test ResetExp", t, func() {
r.resetExp()
So(r.gd.expPerMin, ShouldResemble, int64(2))
})
}
func benchCompareInstance(b *testing.B, src *model.Instance, i *model.Instance) {
if src.Appid != i.Appid || src.Env != i.Env || src.Hostname != i.Hostname ||
src.Region != i.Region {
b.Errorf("instance compare error")
}
}
func register(t *testing.T, is ...*model.Instance) (r *Registry) {
Convey("test register", t, func() {
r = NewRegistry()
var num int
for _, i := range is {
err := r.Register(i, 0)
So(err, ShouldBeNil)
if i.Appid == "main.arch.test" {
num++
}
}
fetchArg := &model.ArgFetch{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: "main.arch.test", Status: 3}
instancesInfo, err := r.Fetch(fetchArg.Zone, fetchArg.Env, fetchArg.Appid, 0, fetchArg.Status)
So(err, ShouldBeNil)
So(len(instancesInfo.Instances), ShouldResemble, num)
})
return r
}
func benchRegister(b *testing.B) (r *Registry, i *model.Instance) {
r = NewRegistry()
i = model.NewInstance(reg)
if err := r.Register(i, 0); err != nil {
b.Errorf("Reigster(%v) error(%v)", i.Appid, err)
}
return r, i
}
func TestEvict(t *testing.T) {
Convey("test evict for protect", t, func() {
r := NewRegistry()
m := model.NewInstance(reg)
// promise the renewtime of instance is expire
m.RenewTimestamp -= 100
err := r.Register(m, 0)
So(err, ShouldBeNil)
// move up the statistics of heartbeat for evict
r.gd.facLastMin = r.gd.facInMin
r.evict()
fetchArg := &model.ArgFetch{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: "main.arch.test", Status: 3}
c, err := r.Fetch(fetchArg.Zone, fetchArg.Env, fetchArg.Appid, 0, fetchArg.Status)
So(err, ShouldBeNil)
// protect
So(len(c.Instances), ShouldResemble, 1)
})
}
func TestEvict2(t *testing.T) {
Convey("test evict for cancel", t, func() {
r := NewRegistry()
m := model.NewInstance(reg)
err := r.Register(m, 0)
So(err, ShouldBeNil)
_, ok := r.Renew(arg)
So(ok, ShouldBeTrue)
// promise the renewtime of instance is expire
m.RenewTimestamp -= int64(time.Second * 100)
r.Register(m, 0)
// move up the statistics of heartbeat for evict
r.gd.facLastMin = r.gd.facInMin
r.evict()
fetchArg := &model.ArgFetch{Region: "shsb", Zone: "sh0001", Env: "pre", Appid: "main.arch.test", Status: 1}
_, err = r.Fetch(fetchArg.Zone, fetchArg.Env, fetchArg.Appid, 0, fetchArg.Status)
So(err, ShouldResemble, ecode.NothingFound)
})
}

View File

@@ -0,0 +1,39 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"discovery.go",
"http.go",
],
importpath = "go-common/app/infra/discovery/http",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/infra/discovery/conf:go_default_library",
"//app/infra/discovery/model:go_default_library",
"//app/infra/discovery/service:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,159 @@
package http
import (
"net/http"
"strconv"
"time"
"go-common/app/infra/discovery/model"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
)
const (
_pollWaitSecond = 30 * time.Second
)
func register(c *bm.Context) {
arg := new(model.ArgRegister)
if err := c.Bind(arg); err != nil {
return
}
i := model.NewInstance(arg)
if i.Status == 0 || i.Status > 2 {
c.JSON(nil, ecode.RequestErr)
return
}
// register replication
if arg.DirtyTimestamp > 0 {
i.DirtyTimestamp = arg.DirtyTimestamp
}
svr.Register(c, i, arg.LatestTimestamp, arg.Replication)
c.JSON(nil, nil)
}
func renew(c *bm.Context) {
arg := new(model.ArgRenew)
if err := c.Bind(arg); err != nil {
return
}
// renew
c.JSON(svr.Renew(c, arg))
}
func cancel(c *bm.Context) {
arg := new(model.ArgCancel)
if err := c.Bind(arg); err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
c.JSON(nil, svr.Cancel(c, arg))
}
func fetchAll(c *bm.Context) {
c.JSON(svr.FetchAll(c), nil)
}
func fetch(c *bm.Context) {
arg := new(model.ArgFetch)
if err := c.Bind(arg); err != nil {
return
}
c.JSON(svr.Fetch(c, arg))
}
func fetchs(c *bm.Context) {
arg := new(model.ArgFetchs)
if err := c.Bind(arg); err != nil {
return
}
c.JSON(svr.Fetchs(c, arg))
}
func poll(c *bm.Context) {
arg := new(model.ArgPolls)
if err := c.Bind(arg); err != nil {
return
}
ch, new, err := svr.Polls(c, arg)
if err != nil && err != ecode.NotModified {
c.JSON(nil, err)
return
}
// wait for instance change
select {
case e := <-ch:
if len(arg.Treeid) != 0 {
c.JSON(e[strconv.FormatInt(arg.Treeid[0], 10)], nil)
} else {
c.JSON(e[arg.Appid[0]], nil)
}
if !new {
svr.DelConns(arg) // broadcast will delete all connections of appid
}
case <-time.After(_pollWaitSecond):
c.JSON(nil, ecode.NotModified)
svr.DelConns(arg)
case <-c.Writer.(http.CloseNotifier).CloseNotify():
c.JSON(nil, ecode.NotModified)
svr.DelConns(arg)
}
}
func polls(c *bm.Context) {
arg := new(model.ArgPolls)
if err := c.Bind(arg); err != nil {
return
}
if len(arg.Treeid) != len(arg.LatestTimestamp) && len(arg.Appid) != len(arg.LatestTimestamp) {
c.JSON(nil, ecode.RequestErr)
return
}
ch, new, err := svr.Polls(c, arg)
if err != nil && err != ecode.NotModified {
c.JSON(nil, err)
return
}
// wait for instance change
select {
case e := <-ch:
c.JSON(e, nil)
if !new {
svr.DelConns(arg) // broadcast will delete all connections of appid
}
case <-time.After(_pollWaitSecond):
c.JSON(nil, ecode.NotModified)
svr.DelConns(arg)
case <-c.Writer.(http.CloseNotifier).CloseNotify():
c.JSON(nil, ecode.NotModified)
svr.DelConns(arg)
}
}
func polling(c *bm.Context) {
arg := new(model.ArgPolling)
if err := c.Bind(arg); err != nil {
return
}
c.JSON(svr.Polling(c, arg))
}
func set(c *bm.Context) {
var (
arg = new(model.ArgSet)
)
if err := c.Bind(arg); err != nil {
return
}
// len of status,metadata must equal to len of hostname or be zero
if (len(arg.Hostname) != len(arg.Status) && len(arg.Status) != 0) ||
(len(arg.Hostname) != len(arg.Metadata) && len(arg.Metadata) != 0) {
c.JSON(nil, ecode.RequestErr)
return
}
c.JSON(nil, svr.Set(c, arg))
}
func nodes(c *bm.Context) {
c.JSON(svr.Nodes(c), nil)
}

View File

@@ -0,0 +1,42 @@
package http
import (
"go-common/app/infra/discovery/conf"
"go-common/app/infra/discovery/service"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
)
var (
svr *service.Service
)
// Init init http
func Init(c *conf.Config, s *service.Service) {
svr = s
engineInner := bm.DefaultServer(c.BM.Inner)
innerRouter(engineInner)
if err := engineInner.Start(); err != nil {
log.Error("bm.DefaultServer error(%v)", err)
panic(err)
}
}
// innerRouter init local router api path.
func innerRouter(e *bm.Engine) {
group := e.Group("/discovery")
{
group.POST("/register", register)
group.POST("/renew", renew)
group.POST("/cancel", cancel)
group.GET("/fetch/all", fetchAll)
group.GET("/fetch", fetch)
group.GET("/fetchs", fetchs)
group.GET("/poll", poll)
group.GET("/polls", polls)
//manager
group.POST("/set", set)
group.GET("/polling", polling)
group.GET("/nodes", nodes)
}
}

View File

@@ -0,0 +1,36 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"instance.go",
"node.go",
"param.go",
],
importpath = "go-common/app/infra/discovery/model",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/ecode:go_default_library",
"//library/log:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,362 @@
package model
import (
"encoding/json"
"strconv"
"sync"
"time"
"go-common/library/ecode"
"go-common/library/log"
)
// InstanceStatus Status of instance
// type InstanceStatus uint32
const (
// InstanceStatusUP Ready to receive traffic
InstanceStatusUP = uint32(1)
// InstancestatusWating Intentionally shutdown for traffic
InstancestatusWating = uint32(1) << 1
)
func (i *Instance) filter(status uint32) bool {
return status&i.Status > 0
}
// Action Replicate type of node
type Action int
const (
// Register Replicate the add action to all nodes
Register Action = iota
// Renew Replicate the heartbeat action to all nodes
Renew
// Cancel Replicate the cancel action to all nodes
Cancel
// Weight Replicate the Weight action to all nodes
Weight
// Delete Replicate the Delete action to all nodes
Delete
// Status Replicate the Status action to all nodes
Status
)
// Instance holds information required for registration with
// <Discovery Server> and to be discovered by other components.
type Instance struct {
Region string `json:"region"`
Zone string `json:"zone"`
Env string `json:"env"`
Appid string `json:"appid"`
Treeid int64 `json:"treeid"`
Hostname string `json:"hostname"`
HTTP string `json:"http"`
RPC string `json:"rpc"`
Version string `json:"version"`
Metadata map[string]string `json:"metadata"`
Addrs []string `json:"addrs"`
// Status enum instance status
Status uint32 `json:"status"`
// timestamp
RegTimestamp int64 `json:"reg_timestamp"`
UpTimestamp int64 `json:"up_timestamp"` // NOTE: It is latest timestamp that status becomes UP.
RenewTimestamp int64 `json:"renew_timestamp"`
DirtyTimestamp int64 `json:"dirty_timestamp"`
LatestTimestamp int64 `json:"latest_timestamp"`
}
// NewInstance new a instance.
func NewInstance(arg *ArgRegister) (i *Instance) {
now := time.Now().UnixNano()
i = &Instance{
Region: arg.Region,
Zone: arg.Zone,
Env: arg.Env,
Appid: arg.Appid,
Treeid: arg.Treeid,
Hostname: arg.Hostname,
HTTP: arg.HTTP,
RPC: arg.RPC,
Version: arg.Version,
Status: arg.Status,
Addrs: arg.Addrs,
RegTimestamp: now,
UpTimestamp: now,
LatestTimestamp: now,
RenewTimestamp: now,
DirtyTimestamp: now,
}
i.Metadata = make(map[string]string)
if arg.Metadata != "" {
if err := json.Unmarshal([]byte(arg.Metadata), &i.Metadata); err != nil {
log.Error("json unmarshal metadata err %v", err)
}
}
return
}
// InstanceInfo the info get by consumer.
type InstanceInfo struct {
Instances []*Instance `json:"instances"`
ZoneInstances map[string][]*Instance `json:"zone_instances"`
LatestTimestamp int64 `json:"latest_timestamp"`
LatestTimestampStr string `json:"latest_timestamp_str"`
}
// Apps app distinguished by zone
type Apps struct {
apps map[string]*App
lock sync.RWMutex
latestTimestamp int64
}
// NewApps return new Apps.
func NewApps() *Apps {
return &Apps{
apps: make(map[string]*App),
}
}
// NewApp news a app by appid. If ok=false, returns the app of already exist.
func (p *Apps) NewApp(zone, appid string, treeid, lts int64) (a *App, new bool) {
p.lock.Lock()
a, ok := p.apps[zone]
if !ok {
a = NewApp(zone, appid, treeid)
p.apps[zone] = a
}
if lts <= p.latestTimestamp {
// insure increase
lts = p.latestTimestamp + 1
}
p.latestTimestamp = lts
p.lock.Unlock()
new = !ok
return
}
// App get app by zone.
func (p *Apps) App(zone string) (as []*App) {
p.lock.RLock()
if zone != "" {
a, ok := p.apps[zone]
if !ok {
p.lock.RUnlock()
return
}
as = []*App{a}
} else {
for _, a := range p.apps {
as = append(as, a)
}
}
p.lock.RUnlock()
return
}
// Del del app by zone.
func (p *Apps) Del(zone string) {
p.lock.Lock()
delete(p.apps, zone)
p.lock.Unlock()
}
// InstanceInfo return slice of instances.if up is true,return all status instance else return up status instance
func (p *Apps) InstanceInfo(zone string, latestTime int64, status uint32) (ci *InstanceInfo, err error) {
p.lock.RLock()
defer p.lock.RUnlock()
if latestTime >= p.latestTimestamp {
err = ecode.NotModified
return
}
ci = &InstanceInfo{
LatestTimestamp: p.latestTimestamp,
LatestTimestampStr: strconv.FormatInt(p.latestTimestamp/int64(time.Second), 10),
ZoneInstances: make(map[string][]*Instance),
}
var ok bool
for z, app := range p.apps {
if zone == "" || z == zone {
ok = true
as := app.Instances()
if len(as) == 0 {
continue
}
instance := make([]*Instance, 0, len(as))
for _, i := range as {
// if up is false return all status instance
if i.filter(status) {
// if i.Status == InstanceStatusUP && i.LatestTimestamp > latestTime { // TODO(felix): increase
ni := new(Instance)
*ni = *i
instance = append(instance, ni)
}
}
ci.Instances = append(ci.Instances, instance...)
ci.ZoneInstances[z] = instance
}
}
if !ok {
err = ecode.NothingFound
} else if len(ci.Instances) == 0 {
err = ecode.NotModified
}
return
}
// UpdateLatest update LatestTimestamp.
func (p *Apps) UpdateLatest(latestTime int64) {
if latestTime <= p.latestTimestamp {
// insure increase
latestTime = p.latestTimestamp + 1
}
p.latestTimestamp = latestTime
}
// App Instances distinguished by hostname
type App struct {
AppID string
Treeid int64
Zone string
instances map[string]*Instance
latestTimestamp int64
lock sync.RWMutex
}
// NewApp new App.
func NewApp(zone, appid string, treeid int64) (a *App) {
a = &App{
Treeid: treeid,
AppID: appid,
Zone: zone,
instances: make(map[string]*Instance),
}
return
}
// Instances return slice of instances.
func (a *App) Instances() (is []*Instance) {
a.lock.RLock()
is = make([]*Instance, 0, len(a.instances))
for _, i := range a.instances {
ni := new(Instance)
*ni = *i
is = append(is, ni)
}
a.lock.RUnlock()
return
}
// NewInstance new a instance.
func (a *App) NewInstance(ni *Instance, latestTime int64) (i *Instance, ok bool) {
i = new(Instance)
a.lock.Lock()
oi, ok := a.instances[ni.Hostname]
if ok {
ni.UpTimestamp = oi.UpTimestamp
if ni.DirtyTimestamp < oi.DirtyTimestamp {
log.Warn("register exist(%v) dirty timestamp over than caller(%v)", oi, ni)
ni = oi
}
}
a.instances[ni.Hostname] = ni
a.updateLatest(latestTime)
*i = *ni
a.lock.Unlock()
ok = !ok
return
}
// Renew new a instance.
func (a *App) Renew(hostname string) (i *Instance, ok bool) {
i = new(Instance)
a.lock.Lock()
defer a.lock.Unlock()
oi, ok := a.instances[hostname]
if !ok {
return
}
oi.RenewTimestamp = time.Now().UnixNano()
*i = *oi
return
}
func (a *App) updateLatest(latestTime int64) {
if latestTime <= a.latestTimestamp {
// insure increase
latestTime = a.latestTimestamp + 1
}
a.latestTimestamp = latestTime
}
// Cancel cancel a instance.
func (a *App) Cancel(hostname string, latestTime int64) (i *Instance, l int, ok bool) {
i = new(Instance)
a.lock.Lock()
defer a.lock.Unlock()
oi, ok := a.instances[hostname]
if !ok {
return
}
delete(a.instances, hostname)
l = len(a.instances)
oi.LatestTimestamp = latestTime
a.updateLatest(latestTime)
*i = *oi
return
}
// Len returns the length of instances.
func (a *App) Len() (l int) {
a.lock.RLock()
l = len(a.instances)
a.lock.RUnlock()
return
}
// Set set new status,metadata of instance .
func (a *App) Set(changes *ArgSet) (ok bool) {
a.lock.Lock()
defer a.lock.Unlock()
var (
dst *Instance
setTime int64
)
if changes.SetTimestamp == 0 {
setTime = time.Now().UnixNano()
}
for i, hostname := range changes.Hostname {
if dst, ok = a.instances[hostname]; !ok {
log.Error("Set hostname(%s) not found", hostname)
return
}
if len(changes.Status) != 0 {
if uint32(changes.Status[i]) != InstanceStatusUP && uint32(changes.Status[i]) != InstancestatusWating {
log.Error("SetStatus change status(%d) is error", changes.Status[i])
ok = false
return
}
dst.Status = uint32(changes.Status[i])
if dst.Status == InstanceStatusUP {
dst.UpTimestamp = setTime
}
}
if len(changes.Metadata) != 0 {
metadata := make(map[string]string)
if err := json.Unmarshal([]byte(changes.Metadata[i]), &metadata); err != nil {
log.Error("set change metadata err %s", changes.Metadata[i])
ok = false
return
}
dst.Metadata = metadata
}
dst.LatestTimestamp = setTime
dst.DirtyTimestamp = setTime
}
a.updateLatest(setTime)
return
}

View File

@@ -0,0 +1,23 @@
package model
// NodeStatus Status of instance
type NodeStatus int
const (
// AppID is discvoery id
AppID = "infra.discovery"
)
const (
// NodeStatusUP Ready to receive register
NodeStatusUP NodeStatus = iota
// NodeStatusLost lost with each other
NodeStatusLost
)
// Node node
type Node struct {
Addr string `json:"addr"`
Status NodeStatus `json:"status"`
Zone string `json:"zone"`
}

View File

@@ -0,0 +1,104 @@
package model
// ArgRegister define register param.
type ArgRegister struct {
Region string `form:"region"`
Zone string `form:"zone" validate:"required"`
Env string `form:"env" validate:"required"`
Appid string `form:"appid" validate:"required"`
Treeid int64 `form:"treeid"`
Hostname string `form:"hostname" validate:"required"`
Status uint32 `form:"status" validate:"required"`
HTTP string `form:"http"`
RPC string `form:"rpc"`
Version string `form:"version"`
Metadata string `form:"metadata"`
Replication bool `form:"replication"`
Addrs []string `form:"addrs,split"`
LatestTimestamp int64 `form:"latest_timestamp"`
DirtyTimestamp int64 `form:"dirty_timestamp"`
}
// ArgRenew define renew params.
type ArgRenew struct {
Region string `form:"region"`
Zone string `form:"zone" validate:"required"`
Env string `form:"env" validate:"required"`
Appid string `form:"appid" validate:"required"`
Treeid int64 `form:"treeid"`
Hostname string `form:"hostname" validate:"required"`
Replication bool `form:"replication"`
DirtyTimestamp int64 `form:"dirty_timestamp"`
}
// ArgCancel define cancel params.
type ArgCancel struct {
Region string `form:"region"`
Zone string `form:"zone" validate:"required"`
Env string `form:"env" validate:"required"`
Appid string `form:"appid" validate:"required"`
Treeid int64 `form:"treeid"`
Hostname string `form:"hostname" validate:"required"`
Replication bool `form:"replication"`
LatestTimestamp int64 `form:"latest_timestamp"`
}
// ArgFetch define fetch param.
type ArgFetch struct {
Region string `form:"region"`
Zone string `form:"zone"`
Env string `form:"env" validate:"required"`
Appid string `form:"appid"`
Treeid int64 `form:"treeid"`
Status uint32 `form:"status" validate:"required"`
}
// ArgFetchs define fetchs arg.
type ArgFetchs struct {
Zone string `form:"zone"`
Env string `form:"env" validate:"required"`
Appid []string `form:"appid,split"`
Status uint32 `form:"status" validate:"required"`
}
// ArgPoll define poll param.
type ArgPoll struct {
Region string `form:"region"`
Zone string `form:"zone"`
Env string `form:"env" validate:"required"`
Appid string `form:"appid"`
Treeid int64 `form:"treeid"`
Hostname string `form:"hostname" validate:"required"`
LatestTimestamp int64 `form:"latest_timestamp"`
}
// ArgPolling define polling arg.
type ArgPolling struct {
Zone string `form:"zone"`
Env string `form:"env" validate:"required"`
Appid string `form:"appid"`
}
// ArgPolls define poll param.
type ArgPolls struct {
Region string `form:"region"`
Zone string `form:"zone"`
Env string `form:"env" validate:"required"`
Appid []string `form:"appid,split"`
Treeid []int64 `form:"treeid,split"`
Hostname string `form:"hostname,split" validate:"required"`
LatestTimestamp []int64 `form:"latest_timestamp,split"`
}
// ArgSet define set param.
type ArgSet struct {
Region string `form:"region"`
Zone string `form:"zone" validate:"required"`
Env string `form:"env" validate:"required"`
Appid string `form:"appid" validate:"required"`
Hostname []string `form:"hostname,split"`
Status []int64 `form:"status,split"`
Metadata []string `form:"metadata"`
Replication bool `form:"replication"`
SetTimestamp int64 `form:"set_timestamp"`
}

View File

@@ -0,0 +1,59 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = ["register_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/infra/discovery/conf:go_default_library",
"//app/infra/discovery/model:go_default_library",
"//library/ecode:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/netutil/breaker:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"register.go",
"service.go",
"syncup.go",
],
importpath = "go-common/app/infra/discovery/service",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/infra/discovery/conf:go_default_library",
"//app/infra/discovery/dao:go_default_library",
"//app/infra/discovery/model:go_default_library",
"//library/conf/env:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,140 @@
package service
import (
"context"
"go-common/app/infra/discovery/model"
"go-common/library/ecode"
"go-common/library/log"
)
// Register a new instance.
func (s *Service) Register(c context.Context, ins *model.Instance, latestTimestamp int64, replication bool) {
s.registry.Register(ins, latestTimestamp)
if ins.Treeid != 0 {
s.tLock.RLock()
appid, ok := s.tree[ins.Treeid]
s.tLock.RUnlock()
if !ok || appid != ins.Appid {
s.tLock.Lock()
s.tree[ins.Treeid] = ins.Appid
s.tLock.Unlock()
}
}
if !replication {
s.nodes.Replicate(c, model.Register, ins, ins.Zone != s.env.Zone)
}
}
// Renew marks the given instance of the given app name as renewed, and also marks whether it originated from replication.
func (s *Service) Renew(c context.Context, arg *model.ArgRenew) (i *model.Instance, err error) {
i, ok := s.registry.Renew(arg)
if !ok {
err = ecode.NothingFound
log.Error("renew appid(%s) hostname(%s) zone(%s) env(%s) error", arg.Appid, arg.Hostname, arg.Zone, arg.Env)
return
}
if !arg.Replication {
s.nodes.Replicate(c, model.Renew, i, arg.Zone != s.env.Zone)
return
}
if arg.DirtyTimestamp > i.DirtyTimestamp {
err = ecode.NothingFound
return
} else if arg.DirtyTimestamp < i.DirtyTimestamp {
err = ecode.Conflict
}
return
}
// Cancel cancels the registration of an instance.
func (s *Service) Cancel(c context.Context, arg *model.ArgCancel) (err error) {
i, ok := s.registry.Cancel(arg)
if !ok {
err = ecode.NothingFound
log.Error("cancel appid(%s) hostname(%s) error", arg.Appid, arg.Hostname)
return
}
if !arg.Replication {
s.nodes.Replicate(c, model.Cancel, i, arg.Zone != s.env.Zone)
}
return
}
// FetchAll fetch all instances of all the department.
func (s *Service) FetchAll(c context.Context) (im map[string][]*model.Instance) {
return s.registry.FetchAll()
}
// Fetchs fetch multi app by appids.
func (s *Service) Fetchs(c context.Context, arg *model.ArgFetchs) (is map[string]*model.InstanceInfo, err error) {
is = make(map[string]*model.InstanceInfo, len(arg.Appid))
for _, appid := range arg.Appid {
i, err := s.registry.Fetch(arg.Zone, arg.Env, appid, 0, arg.Status)
if err != nil {
log.Error("Fetchs fetch appid(%s) err", err)
continue
}
is[appid] = i
}
return
}
// Fetch fetch all instances by appid.
func (s *Service) Fetch(c context.Context, arg *model.ArgFetch) (info *model.InstanceInfo, err error) {
var appid string
if arg.Treeid != 0 {
s.tLock.RLock()
appid = s.tree[arg.Treeid]
s.tLock.RUnlock()
}
if appid == "" {
appid = arg.Appid
}
return s.registry.Fetch(arg.Zone, arg.Env, appid, 0, arg.Status)
}
// Polls hangs request and then write instances when that has changes, or return NotModified.
func (s *Service) Polls(c context.Context, arg *model.ArgPolls) (ch chan map[string]*model.InstanceInfo, new bool, err error) {
var appids []string
s.tLock.RLock()
if len(arg.Treeid) > 0 {
appids = make([]string, 0, len(arg.Treeid))
}
for _, tid := range arg.Treeid {
appid := s.tree[tid]
appids = append(appids, appid)
}
s.tLock.RUnlock()
if len(appids) != 0 {
arg.Appid = appids
}
return s.registry.Polls(arg)
}
// Polling get polling clients.
func (s *Service) Polling(c context.Context, arg *model.ArgPolling) (res []string, err error) {
return s.registry.Polling(arg)
}
// DelConns delete conn of host in appid
func (s *Service) DelConns(arg *model.ArgPolls) {
s.registry.DelConns(arg)
}
// Set set the status of instance by hostnames.
func (s *Service) Set(c context.Context, arg *model.ArgSet) (err error) {
if ok := s.registry.Set(c, arg); !ok {
err = ecode.NothingFound
return
}
if !arg.Replication {
s.nodes.ReplicateSet(c, arg, arg.Zone != s.env.Zone)
}
return
}
// Nodes get all nodes of discovery.
func (s *Service) Nodes(c context.Context) (nsi []*model.Node) {
return s.nodes.Nodes()
}

View File

@@ -0,0 +1,229 @@
package service
import (
"context"
"fmt"
"testing"
"time"
dc "go-common/app/infra/discovery/conf"
"go-common/app/infra/discovery/model"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/netutil/breaker"
xtime "go-common/library/time"
. "github.com/smartystreets/goconvey/convey"
)
var (
ctx = context.TODO()
reg = defRegisArg()
rew = &model.ArgRenew{Appid: "main.arch.test", Hostname: "test1", Region: "shsb", Zone: "sh001", Env: "pre"}
cancel = &model.ArgCancel{Appid: "main.arch.test", Hostname: "test1", Region: "shsb", Zone: "sh001", Env: "pre"}
fet = &model.ArgFetch{Appid: "main.arch.test", Region: "shsb", Zone: "sh001", Env: "pre", Status: 1}
set = &model.ArgSet{Appid: "main.arch.test", Region: "shsb", Hostname: []string{"test1"}, Zone: "sh001", Env: "pre"}
pollArg = newPoll()
)
func newFetchArg() *model.ArgFetchs {
return &model.ArgFetchs{Appid: []string{"main.arch.test"}, Zone: "sh001", Env: "pre", Status: 1}
}
func newPoll() *model.ArgPolls {
return &model.ArgPolls{
Region: "shsb",
Env: "pre",
Appid: []string{"main.arch.test"},
LatestTimestamp: []int64{0},
}
}
func defRegisArg() *model.ArgRegister {
return &model.ArgRegister{
LatestTimestamp: time.Now().Unix(),
Appid: "main.arch.test",
Hostname: "test1", RPC: "127.0.0.1:8080",
Region: "shsb", Zone: "sh001",
Env: "pre", Status: 1,
Metadata: `{"test":"test","weight":"10"}`,
}
}
var config = newConfig()
func newConfig() *dc.Config {
return &dc.Config{HTTPClient: &bm.ClientConfig{Timeout: xtime.Duration(time.Second), Breaker: &breaker.Config{Window: xtime.Duration(time.Second),
Sleep: xtime.Duration(time.Millisecond * 100),
Bucket: 10,
Ratio: 0.5,
Request: 100},
App: &bm.App{Key: "0c4b8fe3ff35a4b6", Secret: "b370880d1aca7d3a289b9b9a7f4d6812"}},
BM: &dc.HTTPServers{Inner: &bm.ServerConfig{Addr: "127.0.0.1:7171"}},
}
}
func TestRegister(t *testing.T) {
Convey("test Register", t, func() {
svr, _ := New(config)
i := model.NewInstance(reg)
svr.Register(context.TODO(), i, reg.LatestTimestamp, false)
ins, err := svr.Fetch(context.TODO(), fet)
for _, i := range ins.Instances {
fmt.Println("ins", i)
}
So(err, ShouldBeNil)
So(len(ins.Instances), ShouldResemble, 1)
Convey("test metadta", func() {
for _, i := range ins.Instances {
So(err, ShouldBeNil)
So(i.Metadata["weight"], ShouldEqual, "10")
So(i.Metadata["test"], ShouldEqual, "test")
}
})
})
}
func TestDiscovery(t *testing.T) {
Convey("test cancel polls", t, func() {
svr, _ := New(config)
reg2 := defRegisArg()
reg2.Hostname = "test2"
i1 := model.NewInstance(reg)
i2 := model.NewInstance(reg2)
svr.Register(context.TODO(), i1, reg.LatestTimestamp, reg.Replication)
svr.Register(context.TODO(), i2, reg2.LatestTimestamp, reg.Replication)
ch, new, err := svr.Polls(context.TODO(), pollArg)
So(err, ShouldBeNil)
So(new, ShouldBeTrue)
ins := <-ch
So(len(ins["main.arch.test"].Instances), ShouldEqual, 2)
pollArg.LatestTimestamp[0] = ins["main.arch.test"].LatestTimestamp
time.Sleep(time.Second)
err = svr.Cancel(context.TODO(), cancel)
So(err, ShouldBeNil)
ch, new, err = svr.Polls(context.TODO(), pollArg)
So(err, ShouldBeNil)
So(new, ShouldBeTrue)
ins = <-ch
So(len(ins["main.arch.test"].Instances), ShouldEqual, 1)
})
Convey("test compatible with treeid polls", t, func() {
svr, _ := New(config)
reg2 := defRegisArg()
reg2.Treeid = 1
i1 := model.NewInstance(reg2)
svr.Register(ctx, i1, reg2.LatestTimestamp, reg2.Replication)
ch, new, err := svr.Polls(context.TODO(), pollArg)
So(err, ShouldBeNil)
So(new, ShouldBeTrue)
ins := <-ch
So(len(ins["main.arch.test"].Instances), ShouldEqual, 1)
treepoll := newPoll()
treepoll.Treeid = []int64{1}
ch, new, err = svr.Polls(context.TODO(), treepoll)
So(err, ShouldBeNil)
So(new, ShouldBeTrue)
ins = <-ch
So(len(ins["1"].Instances), ShouldEqual, 1)
})
}
func TestFetchs(t *testing.T) {
Convey("test fetch multi appid", t, func() {
svr, _ := New(config)
reg2 := defRegisArg()
reg2.Appid = "appid2"
i1 := model.NewInstance(reg)
i2 := model.NewInstance(reg2)
svr.Register(context.TODO(), i1, reg.LatestTimestamp, reg.Replication)
svr.Register(context.TODO(), i2, reg2.LatestTimestamp, reg.Replication)
fetchs := newFetchArg()
fetchs.Appid = append(fetchs.Appid, "appid2")
is, err := svr.Fetchs(ctx, fetchs)
So(err, ShouldBeNil)
So(len(is), ShouldResemble, 2)
})
}
func TestZones(t *testing.T) {
Convey("test multi zone discovery", t, func() {
svr, _ := New(config)
reg2 := defRegisArg()
reg2.Zone = "sh002"
i1 := model.NewInstance(reg)
i2 := model.NewInstance(reg2)
svr.Register(context.TODO(), i1, reg.LatestTimestamp, reg.Replication)
svr.Register(context.TODO(), i2, reg2.LatestTimestamp, reg.Replication)
ch, new, err := svr.Polls(context.TODO(), pollArg)
So(err, ShouldBeNil)
So(new, ShouldBeTrue)
ins := <-ch
So(len(ins["main.arch.test"].ZoneInstances), ShouldEqual, 2)
pollArg.Zone = "sh002"
ch, new, err = svr.Polls(context.TODO(), pollArg)
So(err, ShouldBeNil)
So(new, ShouldBeTrue)
ins = <-ch
So(len(ins["main.arch.test"].ZoneInstances), ShouldEqual, 1)
Convey("test zone update", func() {
pollArg.LatestTimestamp = []int64{ins["main.arch.test"].LatestTimestamp}
pollArg.Zone = ""
reg3 := defRegisArg()
reg3.Zone = "sh002"
reg3.Hostname = "test03"
i3 := model.NewInstance(reg3)
svr.Register(context.TODO(), i3, reg3.LatestTimestamp, reg3.Replication)
ch, new, err = svr.Polls(context.TODO(), pollArg)
So(err, ShouldBeNil)
ins = <-ch
So(len(ins["main.arch.test"].ZoneInstances), ShouldResemble, 2)
So(len(ins["main.arch.test"].ZoneInstances["sh002"]), ShouldResemble, 2)
So(len(ins["main.arch.test"].ZoneInstances["sh001"]), ShouldResemble, 1)
pollArg.LatestTimestamp = []int64{ins["main.arch.test"].LatestTimestamp}
_, _, err = svr.Polls(context.TODO(), pollArg)
So(err, ShouldResemble, ecode.NotModified)
})
})
}
func TestRenew(t *testing.T) {
Convey("test Renew", t, func() {
svr, _ := New(config)
i := model.NewInstance(reg)
svr.Register(context.TODO(), i, reg.LatestTimestamp, reg.Replication)
_, err := svr.Renew(context.TODO(), rew)
So(err, ShouldBeNil)
})
}
func TestCancel(t *testing.T) {
Convey("test cancel", t, func() {
svr, _ := New(config)
i := model.NewInstance(reg)
svr.Register(context.TODO(), i, reg.LatestTimestamp, reg.Replication)
err := svr.Cancel(context.TODO(), cancel)
So(err, ShouldBeNil)
_, err = svr.Fetch(context.TODO(), fet)
So(err, ShouldResemble, ecode.NothingFound)
})
}
func TestFetchAll(t *testing.T) {
Convey("test fetch all", t, func() {
svr, _ := New(config)
i := model.NewInstance(reg)
svr.Register(context.TODO(), i, reg.LatestTimestamp, reg.Replication)
fs := svr.FetchAll(context.TODO())
_, ok := fs[reg.Appid]
So(ok, ShouldBeTrue)
})
}
func TestSet(t *testing.T) {
Convey("test set", t, func() {
svr, _ := New(config)
i := model.NewInstance(reg)
svr.Register(context.TODO(), i, reg.LatestTimestamp, reg.Replication)
set.Metadata = []string{`{"weight":"1"}`}
err := svr.Set(context.TODO(), set)
So(err, ShouldBeNil)
cm, err := svr.Fetch(context.TODO(), fet)
So(err, ShouldBeNil)
So(cm.Instances[0].Metadata["weight"], ShouldResemble, "1")
})
}

View File

@@ -0,0 +1,50 @@
package service
import (
"context"
"os"
"sync"
"go-common/app/infra/discovery/conf"
"go-common/app/infra/discovery/dao"
bm "go-common/library/net/http/blademaster"
)
// Service discovery main service
type Service struct {
c *conf.Config
client *bm.Client
registry *dao.Registry
nodes *dao.Nodes
tLock sync.RWMutex
tree map[int64]string // treeid->appid
env *env
}
type env struct {
Region string
Zone string
}
// New get a discovery service
func New(c *conf.Config) (s *Service, cancel context.CancelFunc) {
s = &Service{
c: c,
client: bm.NewClient(c.HTTPClient),
registry: dao.NewRegistry(),
nodes: dao.NewNodes(c),
tree: make(map[int64]string),
}
s.getEnv()
s.syncUp()
cancel = s.regSelf()
go s.nodesproc()
return
}
func (s *Service) getEnv() {
s.env = &env{
Region: os.Getenv("REGION"),
Zone: os.Getenv("ZONE"),
}
}

View File

@@ -0,0 +1,161 @@
package service
import (
"context"
"fmt"
"net/url"
"time"
"go-common/app/infra/discovery/conf"
"go-common/app/infra/discovery/dao"
"go-common/app/infra/discovery/model"
libenv "go-common/library/conf/env"
"go-common/library/ecode"
"go-common/library/log"
)
var (
_fetchAllURL = "http://%s/discovery/fetch/all"
)
// syncUp populates the registry information from a peer eureka node.
func (s *Service) syncUp() (err error) {
for _, node := range s.nodes.AllNodes() {
if s.nodes.Myself(node.Addr) {
continue
}
uri := fmt.Sprintf(_fetchAllURL, node.Addr)
var res struct {
Code int `json:"code"`
Data map[string][]*model.Instance `json:"data"`
}
if err = s.client.Get(context.TODO(), uri, "", nil, &res); err != nil {
log.Error("e.client.Get(%v) error(%v)", uri, err)
continue
}
if res.Code != 0 {
log.Error("service syncup from(%s) failed ", uri)
continue
}
for _, is := range res.Data {
for _, i := range is {
s.tLock.RLock()
appid, ok := s.tree[i.Treeid]
s.tLock.RUnlock()
if !ok || appid != i.Appid {
s.tLock.Lock()
s.tree[i.Treeid] = i.Appid
s.tLock.Unlock()
}
s.registry.Register(i, i.LatestTimestamp)
}
}
// NOTE: no return, make sure that all instances from other nodes register into self.
}
s.nodes.UP()
return
}
func (s *Service) regSelf() context.CancelFunc {
ctx, cancel := context.WithCancel(context.Background())
now := time.Now().UnixNano()
ins := &model.Instance{
Region: libenv.Region,
Zone: libenv.Zone,
Env: libenv.DeployEnv,
Hostname: libenv.Hostname,
Appid: model.AppID,
Addrs: []string{
"http://" + s.c.BM.Inner.Addr,
},
Status: model.InstanceStatusUP,
RegTimestamp: now,
UpTimestamp: now,
LatestTimestamp: now,
RenewTimestamp: now,
DirtyTimestamp: now,
}
s.Register(ctx, ins, now, false)
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
arg := &model.ArgRenew{
Appid: model.AppID,
Region: libenv.Region,
Zone: libenv.Zone,
Env: libenv.DeployEnv,
Hostname: libenv.Hostname,
}
if _, err := s.Renew(ctx, arg); err != nil && ecode.NothingFound.Equal(err) {
s.Register(ctx, ins, now, false)
}
case <-ctx.Done():
arg := &model.ArgCancel{
Appid: model.AppID,
Region: libenv.Region,
Zone: libenv.Zone,
Env: libenv.DeployEnv,
Hostname: libenv.Hostname,
}
if err := s.Cancel(context.Background(), arg); err != nil {
log.Error("s.Cancel(%+v) error(%v)", arg, err)
}
return
}
}
}()
return cancel
}
func (s *Service) nodesproc() {
var (
lastTs int64
)
for {
arg := &model.ArgPolls{
Appid: []string{model.AppID},
Region: libenv.Region,
Env: libenv.DeployEnv,
Hostname: libenv.Hostname,
LatestTimestamp: []int64{lastTs},
}
ch, _, err := s.registry.Polls(arg)
if err != nil && err != ecode.NotModified {
log.Error("s.registry(%v) error(%v)", arg, err)
time.Sleep(time.Second)
continue
}
apps := <-ch
ins, ok := apps[model.AppID]
if !ok || ins == nil {
return
}
var (
nodes []string
zones = make(map[string][]string)
)
for _, in := range ins.Instances {
for _, addr := range in.Addrs {
u, err := url.Parse(addr)
if err == nil && u.Scheme == "http" {
if in.Zone == libenv.Zone {
nodes = append(nodes, u.Host)
} else if _, ok := s.c.Zones[in.Zone]; ok {
zones[in.Zone] = append(zones[in.Zone], u.Host)
}
}
}
}
lastTs = ins.LatestTimestamp
log.Info("discovery changed nodes:%v zones:%v", nodes, zones)
c := new(conf.Config)
*c = *s.c
c.Nodes = nodes
c.Zones = zones
s.nodes = dao.NewNodes(c)
s.nodes.UP()
}
}