feat: registry.
This commit is contained in:
@@ -1,28 +1,357 @@
|
||||
package eureka
|
||||
|
||||
import (
|
||||
eurekaKratos "github.com/go-kratos/kratos/contrib/registry/eureka/v2"
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
|
||||
conf "github.com/tx7do/kratos-bootstrap/api/gen/go/conf/v1"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewRegistry 创建一个注册发现客户端 - Eureka
|
||||
func NewRegistry(c *conf.Registry) *eurekaKratos.Registry {
|
||||
if c == nil || c.Eureka == nil {
|
||||
const (
|
||||
statusUp = "UP"
|
||||
statusDown = "DOWN"
|
||||
statusOutOfService = "OUT_OF_SERVICE"
|
||||
heartbeatRetry = 3
|
||||
maxIdleConns = 100
|
||||
heartbeatTime = 10 * time.Second
|
||||
httpTimeout = 3 * time.Second
|
||||
refreshTime = 30 * time.Second
|
||||
)
|
||||
|
||||
type Endpoint struct {
|
||||
InstanceID string
|
||||
IP string
|
||||
AppID string
|
||||
Port int
|
||||
SecurePort int
|
||||
HomePageURL string
|
||||
StatusPageURL string
|
||||
HealthCheckURL string
|
||||
MetaData map[string]string
|
||||
}
|
||||
|
||||
// ApplicationsRootResponse for /eureka/apps
|
||||
type ApplicationsRootResponse struct {
|
||||
ApplicationsResponse `json:"applications"`
|
||||
}
|
||||
|
||||
type ApplicationsResponse struct {
|
||||
Version string `json:"versions__delta"`
|
||||
AppsHashcode string `json:"apps__hashcode"`
|
||||
Applications []Application `json:"application"`
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
Name string `json:"name"`
|
||||
Instance []Instance `json:"instance"`
|
||||
}
|
||||
|
||||
type RequestInstance struct {
|
||||
Instance Instance `json:"instance"`
|
||||
}
|
||||
|
||||
type Instance struct {
|
||||
InstanceID string `json:"instanceId"`
|
||||
HostName string `json:"hostName"`
|
||||
Port Port `json:"port"`
|
||||
App string `json:"app"`
|
||||
IPAddr string `json:"ipAddr"`
|
||||
VipAddress string `json:"vipAddress"`
|
||||
Status string `json:"status"`
|
||||
SecurePort Port `json:"securePort"`
|
||||
HomePageURL string `json:"homePageUrl"`
|
||||
StatusPageURL string `json:"statusPageUrl"`
|
||||
HealthCheckURL string `json:"healthCheckUrl"`
|
||||
DataCenterInfo DataCenterInfo `json:"dataCenterInfo"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
type Port struct {
|
||||
Port int `json:"$"`
|
||||
Enabled string `json:"@enabled"`
|
||||
}
|
||||
|
||||
type DataCenterInfo struct {
|
||||
Name string `json:"name"`
|
||||
Class string `json:"@class"`
|
||||
}
|
||||
|
||||
var _ APIInterface = (*Client)(nil)
|
||||
|
||||
type APIInterface interface {
|
||||
Register(ctx context.Context, ep Endpoint) error
|
||||
Deregister(ctx context.Context, appID, instanceID string) error
|
||||
Heartbeat(ep Endpoint)
|
||||
FetchApps(ctx context.Context) []Application
|
||||
FetchAllUpInstances(ctx context.Context) []Instance
|
||||
FetchAppInstances(ctx context.Context, appID string) (m Application, err error)
|
||||
FetchAppUpInstances(ctx context.Context, appID string) []Instance
|
||||
FetchAppInstance(ctx context.Context, appID string, instanceID string) (m Instance, err error)
|
||||
FetchInstance(ctx context.Context, instanceID string) (m Instance, err error)
|
||||
Out(ctx context.Context, appID, instanceID string) error
|
||||
Down(ctx context.Context, appID, instanceID string) error
|
||||
}
|
||||
|
||||
type ClientOption func(e *Client)
|
||||
|
||||
func WithMaxRetry(maxRetry int) ClientOption {
|
||||
return func(e *Client) { e.maxRetry = maxRetry }
|
||||
}
|
||||
|
||||
func WithHeartbeatInterval(interval time.Duration) ClientOption {
|
||||
return func(e *Client) {
|
||||
e.heartbeatInterval = interval
|
||||
}
|
||||
}
|
||||
|
||||
func WithClientContext(ctx context.Context) ClientOption {
|
||||
return func(e *Client) { e.ctx = ctx }
|
||||
}
|
||||
|
||||
func WithNamespace(path string) ClientOption {
|
||||
return func(e *Client) { e.eurekaPath = path }
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ctx context.Context
|
||||
urls []string
|
||||
eurekaPath string
|
||||
maxRetry int
|
||||
heartbeatInterval time.Duration
|
||||
client *http.Client
|
||||
keepalive map[string]chan struct{}
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewClient(urls []string, opts ...ClientOption) *Client {
|
||||
tr := &http.Transport{
|
||||
MaxIdleConns: maxIdleConns,
|
||||
}
|
||||
|
||||
e := &Client{
|
||||
ctx: context.Background(),
|
||||
urls: urls,
|
||||
eurekaPath: "eureka/v2",
|
||||
maxRetry: len(urls),
|
||||
heartbeatInterval: heartbeatTime,
|
||||
client: &http.Client{Transport: tr, Timeout: httpTimeout},
|
||||
keepalive: make(map[string]chan struct{}),
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(e)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Client) FetchApps(ctx context.Context) []Application {
|
||||
var m ApplicationsRootResponse
|
||||
if err := e.do(ctx, http.MethodGet, []string{"apps"}, nil, &m); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var opts []eurekaKratos.Option
|
||||
opts = append(opts, eurekaKratos.WithHeartbeat(c.Eureka.HeartbeatInterval.AsDuration()))
|
||||
opts = append(opts, eurekaKratos.WithRefresh(c.Eureka.RefreshInterval.AsDuration()))
|
||||
opts = append(opts, eurekaKratos.WithEurekaPath(c.Eureka.Path))
|
||||
return m.Applications
|
||||
}
|
||||
|
||||
var err error
|
||||
var reg *eurekaKratos.Registry
|
||||
if reg, err = eurekaKratos.New(c.Eureka.Endpoints, opts...); err != nil {
|
||||
log.Fatal(err)
|
||||
func (e *Client) FetchAppInstances(ctx context.Context, appID string) (m Application, err error) {
|
||||
err = e.do(ctx, http.MethodGet, []string{"apps", appID}, nil, &m)
|
||||
return
|
||||
}
|
||||
|
||||
func (e *Client) FetchAppUpInstances(ctx context.Context, appID string) []Instance {
|
||||
app, err := e.FetchAppInstances(ctx, appID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return e.filterUp(app)
|
||||
}
|
||||
|
||||
func (e *Client) FetchAppInstance(ctx context.Context, appID string, instanceID string) (m Instance, err error) {
|
||||
err = e.do(ctx, http.MethodGet, []string{"apps", appID, instanceID}, nil, &m)
|
||||
return
|
||||
}
|
||||
|
||||
func (e *Client) FetchInstance(ctx context.Context, instanceID string) (m Instance, err error) {
|
||||
err = e.do(ctx, http.MethodGet, []string{"instances", instanceID}, nil, &m)
|
||||
return
|
||||
}
|
||||
|
||||
func (e *Client) Out(ctx context.Context, appID, instanceID string) error {
|
||||
return e.do(ctx, http.MethodPut, []string{"apps", appID, instanceID, fmt.Sprintf("status?value=%s", statusOutOfService)}, nil, nil)
|
||||
}
|
||||
|
||||
func (e *Client) Down(ctx context.Context, appID, instanceID string) error {
|
||||
return e.do(ctx, http.MethodPut, []string{"apps", appID, instanceID, fmt.Sprintf("status?value=%s", statusDown)}, nil, nil)
|
||||
}
|
||||
|
||||
func (e *Client) FetchAllUpInstances(ctx context.Context) []Instance {
|
||||
return e.filterUp(e.FetchApps(ctx)...)
|
||||
}
|
||||
|
||||
func (e *Client) Register(ctx context.Context, ep Endpoint) error {
|
||||
return e.registerEndpoint(ctx, ep)
|
||||
}
|
||||
|
||||
func (e *Client) Deregister(ctx context.Context, appID, instanceID string) error {
|
||||
if err := e.do(ctx, http.MethodDelete, []string{"apps", appID, instanceID}, nil, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
go e.cancelHeartbeat(appID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Client) registerEndpoint(ctx context.Context, ep Endpoint) error {
|
||||
instance := RequestInstance{
|
||||
Instance: Instance{
|
||||
InstanceID: ep.InstanceID,
|
||||
HostName: ep.AppID,
|
||||
Port: Port{
|
||||
Port: ep.Port,
|
||||
Enabled: "true",
|
||||
},
|
||||
App: ep.AppID,
|
||||
IPAddr: ep.IP,
|
||||
VipAddress: ep.AppID,
|
||||
Status: statusUp,
|
||||
SecurePort: Port{
|
||||
Port: ep.SecurePort,
|
||||
Enabled: "false",
|
||||
},
|
||||
HomePageURL: ep.HomePageURL,
|
||||
StatusPageURL: ep.StatusPageURL,
|
||||
HealthCheckURL: ep.HealthCheckURL,
|
||||
DataCenterInfo: DataCenterInfo{
|
||||
Name: "MyOwn",
|
||||
Class: "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
|
||||
},
|
||||
Metadata: ep.MetaData,
|
||||
},
|
||||
}
|
||||
|
||||
return reg
|
||||
body, err := json.Marshal(instance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.do(ctx, http.MethodPost, []string{"apps", ep.AppID}, bytes.NewReader(body), nil)
|
||||
}
|
||||
|
||||
func (e *Client) Heartbeat(ep Endpoint) {
|
||||
e.lock.Lock()
|
||||
e.keepalive[ep.AppID] = make(chan struct{})
|
||||
e.lock.Unlock()
|
||||
|
||||
ticker := time.NewTicker(e.heartbeatInterval)
|
||||
defer ticker.Stop()
|
||||
retryCount := 0
|
||||
for {
|
||||
select {
|
||||
case <-e.ctx.Done():
|
||||
return
|
||||
case <-e.keepalive[ep.AppID]:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := e.do(e.ctx, http.MethodPut, []string{"apps", ep.AppID, ep.InstanceID}, nil, nil); err != nil {
|
||||
if retryCount++; retryCount > heartbeatRetry {
|
||||
_ = e.registerEndpoint(e.ctx, ep)
|
||||
retryCount = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Client) cancelHeartbeat(appID string) {
|
||||
e.lock.Lock()
|
||||
defer e.lock.Unlock()
|
||||
if ch, ok := e.keepalive[appID]; ok {
|
||||
ch <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Client) filterUp(apps ...Application) (res []Instance) {
|
||||
for _, app := range apps {
|
||||
for _, ins := range app.Instance {
|
||||
if ins.Status == statusUp {
|
||||
res = append(res, ins)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (e *Client) pickServer(currentTimes int) string {
|
||||
return e.urls[currentTimes%e.maxRetry]
|
||||
}
|
||||
|
||||
func (e *Client) shuffle() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
rand.Shuffle(len(e.urls), func(i, j int) {
|
||||
e.urls[i], e.urls[j] = e.urls[j], e.urls[i]
|
||||
})
|
||||
}
|
||||
|
||||
func (e *Client) buildAPI(currentTimes int, params ...string) string {
|
||||
if currentTimes == 0 {
|
||||
e.shuffle()
|
||||
}
|
||||
server := e.pickServer(currentTimes)
|
||||
params = append([]string{server, e.eurekaPath}, params...)
|
||||
return strings.Join(params, "/")
|
||||
}
|
||||
|
||||
func (e *Client) request(ctx context.Context, method string, params []string, input io.Reader, output interface{}, i int) (bool, error) {
|
||||
request, err := http.NewRequestWithContext(ctx, method, e.buildAPI(i, params...), input)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
request.Header.Add("User-Agent", "go-eureka-client")
|
||||
request.Header.Add("Accept", "application/json;charset=UTF-8")
|
||||
request.Header.Add("Content-Type", "application/json;charset=UTF-8")
|
||||
resp, err := e.client.Do(request)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if output != nil && resp.StatusCode/100 == 2 {
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
err = json.Unmarshal(data, output)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
return false, fmt.Errorf("response Error %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (e *Client) do(ctx context.Context, method string, params []string, input io.Reader, output interface{}) error {
|
||||
for i := 0; i < e.maxRetry; i++ {
|
||||
retry, err := e.request(ctx, method, params, input, output, i)
|
||||
if retry {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("retry after %d times", e.maxRetry)
|
||||
}
|
||||
|
||||
33
registry/eureka/client_creator.go
Normal file
33
registry/eureka/client_creator.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package eureka
|
||||
|
||||
import (
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
|
||||
conf "github.com/tx7do/kratos-bootstrap/api/gen/go/conf/v1"
|
||||
)
|
||||
|
||||
// NewRegistry 创建一个注册发现客户端 - Eureka
|
||||
func NewRegistry(c *conf.Registry) *Registry {
|
||||
if c == nil || c.Eureka == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var opts []Option
|
||||
if c.Eureka.HeartbeatInterval != nil {
|
||||
opts = append(opts, WithHeartbeat(c.Eureka.HeartbeatInterval.AsDuration()))
|
||||
}
|
||||
if c.Eureka.RefreshInterval != nil {
|
||||
opts = append(opts, WithRefresh(c.Eureka.RefreshInterval.AsDuration()))
|
||||
}
|
||||
if c.Eureka.Path != "" {
|
||||
opts = append(opts, WithEurekaPath(c.Eureka.Path))
|
||||
}
|
||||
|
||||
var err error
|
||||
var reg *Registry
|
||||
if reg, err = New(c.Eureka.Endpoints, opts...); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return reg
|
||||
}
|
||||
137
registry/eureka/eureka.go
Normal file
137
registry/eureka/eureka.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package eureka
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type subscriber struct {
|
||||
appID string
|
||||
callBack func()
|
||||
}
|
||||
|
||||
type API struct {
|
||||
cli *Client
|
||||
allInstances map[string][]Instance
|
||||
subscribers map[string]*subscriber
|
||||
refreshInterval time.Duration
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewAPI(ctx context.Context, client *Client, refreshInterval time.Duration) *API {
|
||||
e := &API{
|
||||
cli: client,
|
||||
allInstances: make(map[string][]Instance),
|
||||
subscribers: make(map[string]*subscriber),
|
||||
refreshInterval: refreshInterval,
|
||||
}
|
||||
|
||||
// it is required to broadcast for the first time
|
||||
go e.broadcast()
|
||||
|
||||
go e.refresh(ctx)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *API) refresh(ctx context.Context) {
|
||||
ticker := time.NewTicker(e.refreshInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
e.broadcast()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *API) broadcast() {
|
||||
instances := e.cacheAllInstances()
|
||||
if instances == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, subscriber := range e.subscribers {
|
||||
go subscriber.callBack()
|
||||
}
|
||||
defer e.lock.Unlock()
|
||||
e.lock.Lock()
|
||||
e.allInstances = instances
|
||||
}
|
||||
|
||||
func (e *API) cacheAllInstances() map[string][]Instance {
|
||||
items := make(map[string][]Instance)
|
||||
instances := e.cli.FetchAllUpInstances(context.Background())
|
||||
for _, instance := range instances {
|
||||
items[e.ToAppID(instance.App)] = append(items[instance.App], instance)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (e *API) Register(ctx context.Context, serviceName string, endpoints ...Endpoint) error {
|
||||
appID := e.ToAppID(serviceName)
|
||||
upInstances := make(map[string]struct{})
|
||||
|
||||
for _, ins := range e.GetService(ctx, appID) {
|
||||
upInstances[ins.InstanceID] = struct{}{}
|
||||
}
|
||||
|
||||
for _, ep := range endpoints {
|
||||
if _, ok := upInstances[ep.InstanceID]; !ok {
|
||||
if err := e.cli.Register(ctx, ep); err != nil {
|
||||
return err
|
||||
}
|
||||
go e.cli.Heartbeat(ep)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deregister ctx is the same as register ctx
|
||||
func (e *API) Deregister(ctx context.Context, endpoints []Endpoint) error {
|
||||
for _, ep := range endpoints {
|
||||
if err := e.cli.Deregister(ctx, ep.AppID, ep.InstanceID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *API) Subscribe(serverName string, fn func()) error {
|
||||
e.lock.Lock()
|
||||
defer e.lock.Unlock()
|
||||
appID := e.ToAppID(serverName)
|
||||
e.subscribers[appID] = &subscriber{
|
||||
appID: appID,
|
||||
callBack: fn,
|
||||
}
|
||||
go e.broadcast()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *API) GetService(ctx context.Context, serverName string) []Instance {
|
||||
appID := e.ToAppID(serverName)
|
||||
if ins, ok := e.allInstances[appID]; ok {
|
||||
return ins
|
||||
}
|
||||
|
||||
// if not in allInstances of API, you can try to obtain it separately again
|
||||
return e.cli.FetchAppUpInstances(ctx, appID)
|
||||
}
|
||||
|
||||
func (e *API) Unsubscribe(serverName string) {
|
||||
e.lock.Lock()
|
||||
defer e.lock.Unlock()
|
||||
delete(e.subscribers, e.ToAppID(serverName))
|
||||
}
|
||||
|
||||
func (e *API) ToAppID(serverName string) string {
|
||||
return strings.ToUpper(serverName)
|
||||
}
|
||||
29
registry/eureka/go.mod
Normal file
29
registry/eureka/go.mod
Normal file
@@ -0,0 +1,29 @@
|
||||
module github.com/tx7do/kratos-bootstrap/registry/eureka
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.3
|
||||
|
||||
replace (
|
||||
github.com/armon/go-metrics => github.com/hashicorp/go-metrics v0.4.1
|
||||
|
||||
github.com/tx7do/kratos-bootstrap/api => ../../api
|
||||
github.com/tx7do/kratos-bootstrap/registry => ../registry
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-kratos/kratos/v2 v2.8.4
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tx7do/kratos-bootstrap/api v0.0.20
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
26
registry/eureka/go.sum
Normal file
26
registry/eureka/go.sum
Normal file
@@ -0,0 +1,26 @@
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-kratos/kratos/v2 v2.8.4 h1:eIJLE9Qq9WSoKx+Buy2uPyrahtF/lPh+Xf4MTpxhmjs=
|
||||
github.com/go-kratos/kratos/v2 v2.8.4/go.mod h1:mq62W2101a5uYyRxe+7IdWubu7gZCGYqSNKwGFiiRcw=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
25
registry/eureka/options.go
Normal file
25
registry/eureka/options.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package eureka
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Option func(o *Registry)
|
||||
|
||||
// WithContext with registry context.
|
||||
func WithContext(ctx context.Context) Option {
|
||||
return func(o *Registry) { o.ctx = ctx }
|
||||
}
|
||||
|
||||
func WithHeartbeat(interval time.Duration) Option {
|
||||
return func(o *Registry) { o.heartbeatInterval = interval }
|
||||
}
|
||||
|
||||
func WithRefresh(interval time.Duration) Option {
|
||||
return func(o *Registry) { o.refreshInterval = interval }
|
||||
}
|
||||
|
||||
func WithEurekaPath(path string) Option {
|
||||
return func(o *Registry) { o.eurekaPath = path }
|
||||
}
|
||||
124
registry/eureka/register.go
Normal file
124
registry/eureka/register.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package eureka
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/registry"
|
||||
)
|
||||
|
||||
var (
|
||||
_ registry.Registrar = (*Registry)(nil)
|
||||
_ registry.Discovery = (*Registry)(nil)
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
ctx context.Context
|
||||
api *API
|
||||
heartbeatInterval time.Duration
|
||||
refreshInterval time.Duration
|
||||
eurekaPath string
|
||||
}
|
||||
|
||||
func New(eurekaUrls []string, opts ...Option) (*Registry, error) {
|
||||
r := &Registry{
|
||||
ctx: context.Background(),
|
||||
heartbeatInterval: heartbeatTime,
|
||||
refreshInterval: refreshTime,
|
||||
eurekaPath: "eureka/v2",
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(r)
|
||||
}
|
||||
|
||||
client := NewClient(eurekaUrls, WithHeartbeatInterval(r.heartbeatInterval), WithClientContext(r.ctx), WithNamespace(r.eurekaPath))
|
||||
r.api = NewAPI(r.ctx, client, r.refreshInterval)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Register 这里的Context是每个注册器独享的
|
||||
func (r *Registry) Register(ctx context.Context, service *registry.ServiceInstance) error {
|
||||
return r.api.Register(ctx, service.Name, r.Endpoints(service)...)
|
||||
}
|
||||
|
||||
// Deregister registry service to zookeeper.
|
||||
func (r *Registry) Deregister(ctx context.Context, service *registry.ServiceInstance) error {
|
||||
return r.api.Deregister(ctx, r.Endpoints(service))
|
||||
}
|
||||
|
||||
// GetService get services from zookeeper
|
||||
func (r *Registry) GetService(ctx context.Context, serviceName string) ([]*registry.ServiceInstance, error) {
|
||||
instances := r.api.GetService(ctx, serviceName)
|
||||
items := make([]*registry.ServiceInstance, 0, len(instances))
|
||||
for _, instance := range instances {
|
||||
items = append(items, ®istry.ServiceInstance{
|
||||
ID: instance.Metadata["ID"],
|
||||
Name: instance.Metadata["Name"],
|
||||
Version: instance.Metadata["Version"],
|
||||
Endpoints: []string{instance.Metadata["Endpoints"]},
|
||||
Metadata: instance.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// Watch 是独立的ctx
|
||||
func (r *Registry) Watch(ctx context.Context, serviceName string) (registry.Watcher, error) {
|
||||
return newWatch(ctx, r.api, serviceName)
|
||||
}
|
||||
|
||||
func (r *Registry) Endpoints(service *registry.ServiceInstance) []Endpoint {
|
||||
res := make([]Endpoint, 0, len(service.Endpoints))
|
||||
for _, ep := range service.Endpoints {
|
||||
start := strings.Index(ep, "//")
|
||||
end := strings.LastIndex(ep, ":")
|
||||
appID := strings.ToUpper(service.Name)
|
||||
ip := ep[start+2 : end]
|
||||
sport := ep[end+1:]
|
||||
port, _ := strconv.Atoi(sport)
|
||||
securePort := 443
|
||||
homePageURL := fmt.Sprintf("%s/", ep)
|
||||
statusPageURL := fmt.Sprintf("%s/info", ep)
|
||||
healthCheckURL := fmt.Sprintf("%s/health", ep)
|
||||
instanceID := strings.Join([]string{ip, appID, sport}, ":")
|
||||
metadata := make(map[string]string)
|
||||
if len(service.Metadata) > 0 {
|
||||
metadata = service.Metadata
|
||||
}
|
||||
if s, ok := service.Metadata["securePort"]; ok {
|
||||
securePort, _ = strconv.Atoi(s)
|
||||
}
|
||||
if s, ok := service.Metadata["homePageURL"]; ok {
|
||||
homePageURL = s
|
||||
}
|
||||
if s, ok := service.Metadata["statusPageURL"]; ok {
|
||||
statusPageURL = s
|
||||
}
|
||||
if s, ok := service.Metadata["healthCheckURL"]; ok {
|
||||
healthCheckURL = s
|
||||
}
|
||||
metadata["ID"] = service.ID
|
||||
metadata["Name"] = service.Name
|
||||
metadata["Version"] = service.Version
|
||||
metadata["Endpoints"] = ep
|
||||
metadata["agent"] = "go-eureka-client"
|
||||
res = append(res, Endpoint{
|
||||
AppID: appID,
|
||||
IP: ip,
|
||||
Port: port,
|
||||
SecurePort: securePort,
|
||||
HomePageURL: homePageURL,
|
||||
StatusPageURL: statusPageURL,
|
||||
HealthCheckURL: healthCheckURL,
|
||||
InstanceID: instanceID,
|
||||
MetaData: metadata,
|
||||
})
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
114
registry/eureka/register_test.go
Normal file
114
registry/eureka/register_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package eureka
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/registry"
|
||||
)
|
||||
|
||||
func TestRegistry(_ *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
s1 := ®istry.ServiceInstance{
|
||||
ID: "0",
|
||||
Name: "helloworld",
|
||||
Endpoints: []string{"http://127.0.0.1:1111"},
|
||||
}
|
||||
s2 := ®istry.ServiceInstance{
|
||||
ID: "0",
|
||||
Name: "helloworld2",
|
||||
Endpoints: []string{"http://127.0.0.1:222"},
|
||||
}
|
||||
|
||||
r, _ := New([]string{"https://127.0.0.1:18761"}, WithContext(ctx), WithHeartbeat(time.Second), WithRefresh(time.Second), WithEurekaPath("eureka"))
|
||||
|
||||
go do(r, s1)
|
||||
go do(r, s2)
|
||||
|
||||
time.Sleep(time.Second * 20)
|
||||
cancel()
|
||||
time.Sleep(time.Second * 1)
|
||||
}
|
||||
|
||||
func do(r *Registry, s *registry.ServiceInstance) {
|
||||
w, err := r.Watch(context.Background(), s.Name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
_ = w.Stop()
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
res, nextErr := w.Next()
|
||||
if nextErr != nil {
|
||||
return
|
||||
}
|
||||
log.Printf("watch: %d", len(res))
|
||||
for _, r := range res {
|
||||
log.Printf("next: %+v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
if err = r.Register(ctx, s); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
res, err := r.GetService(ctx, s.Name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for i, re := range res {
|
||||
log.Printf("first %d re:%v\n", i, re)
|
||||
}
|
||||
|
||||
if len(res) != 1 && res[0].Name != s.Name {
|
||||
log.Fatalf("not expected: %+v", res)
|
||||
}
|
||||
|
||||
if err = r.Deregister(ctx, s); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cancel()
|
||||
time.Sleep(time.Second * 10)
|
||||
|
||||
res, err = r.GetService(ctx, s.Name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for i, re := range res {
|
||||
log.Printf("second %d re:%v\n", i, re)
|
||||
}
|
||||
if len(res) != 0 {
|
||||
log.Fatalf("not expected empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLock(_ *testing.T) {
|
||||
type me struct {
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
a := &me{}
|
||||
go func() {
|
||||
defer a.lock.Unlock()
|
||||
a.lock.Lock()
|
||||
fmt.Println("This is fmt first.")
|
||||
time.Sleep(time.Second * 5)
|
||||
}()
|
||||
go func() {
|
||||
defer a.lock.Unlock()
|
||||
a.lock.Lock()
|
||||
fmt.Println("This is fmt second.")
|
||||
time.Sleep(time.Second * 5)
|
||||
}()
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
60
registry/eureka/watcher.go
Normal file
60
registry/eureka/watcher.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package eureka
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/registry"
|
||||
)
|
||||
|
||||
var _ registry.Watcher = (*watcher)(nil)
|
||||
|
||||
type watcher struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
cli *API
|
||||
watchChan chan struct{}
|
||||
serverName string
|
||||
}
|
||||
|
||||
func newWatch(ctx context.Context, cli *API, serverName string) (*watcher, error) {
|
||||
w := &watcher{
|
||||
ctx: ctx,
|
||||
cli: cli,
|
||||
serverName: serverName,
|
||||
watchChan: make(chan struct{}, 1),
|
||||
}
|
||||
w.ctx, w.cancel = context.WithCancel(ctx)
|
||||
e := w.cli.Subscribe(
|
||||
serverName,
|
||||
func() {
|
||||
w.watchChan <- struct{}{}
|
||||
},
|
||||
)
|
||||
return w, e
|
||||
}
|
||||
|
||||
func (w *watcher) Next() (services []*registry.ServiceInstance, err error) {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return nil, w.ctx.Err()
|
||||
case <-w.watchChan:
|
||||
instances := w.cli.GetService(w.ctx, w.serverName)
|
||||
services = make([]*registry.ServiceInstance, 0, len(instances))
|
||||
for _, instance := range instances {
|
||||
services = append(services, ®istry.ServiceInstance{
|
||||
ID: instance.Metadata["ID"],
|
||||
Name: instance.Metadata["Name"],
|
||||
Version: instance.Metadata["Version"],
|
||||
Endpoints: []string{instance.Metadata["Endpoints"]},
|
||||
Metadata: instance.Metadata,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) Stop() error {
|
||||
w.cancel()
|
||||
w.cli.Unsubscribe(w.serverName)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user