You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
kratos/contrib/registry/eureka/client.go

358 lines
9.2 KiB

package eureka
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"strings"
"sync"
"time"
)
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
}
return m.Applications
}
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,
},
}
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)
}