feat: registry.

This commit is contained in:
Bobo
2025-06-02 11:53:18 +08:00
parent b0e91998e1
commit 59c731904a
83 changed files with 7154 additions and 1784 deletions

View File

@@ -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)
}

View 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
View 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
View 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
View 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=

View 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
View 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, &registry.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
}

View 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 := &registry.ServiceInstance{
ID: "0",
Name: "helloworld",
Endpoints: []string{"http://127.0.0.1:1111"},
}
s2 := &registry.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)
}

View 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, &registry.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
}