节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
4.69 KiB
Stardust
package stardust

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"sync"
	"time"
)

// ConfigInfo 配置信息
type ConfigInfo struct {
	Version     int               `json:"Version"`
	Scope       string            `json:"Scope"`
	SourceIP    string            `json:"SourceIP"`
	NextVersion int               `json:"NextVersion"`
	NextPublish string            `json:"NextPublish"`
	UpdateTime  int64             `json:"UpdateTime"`
	Configs     map[string]string `json:"Configs"`
}

// ConfigClient 配置中心客户端
type ConfigClient struct {
	Server   string
	AppID    string
	Secret   string
	ClientID string

	token       string
	tokenExpire int64
	version     int
	configs     map[string]string
	mu          sync.RWMutex
	running     bool
	stopCh      chan struct{}
	client      *http.Client
	onChange    func(map[string]string)
}

// NewConfigClient 创建配置客户端
func NewConfigClient(server, appID, secret string) *ConfigClient {
	return &ConfigClient{
		Server:   server,
		AppID:    appID,
		Secret:   secret,
		ClientID: fmt.Sprintf("%s@%d", getLocalIP(), os.Getpid()),
		configs:  make(map[string]string),
		stopCh:   make(chan struct{}),
		client:   &http.Client{Timeout: 10 * time.Second},
	}
}

// Start 启动配置客户端
func (c *ConfigClient) Start() {
	c.login()
	c.running = true
	go c.pollLoop()
}

// Stop 停止配置客户端
func (c *ConfigClient) Stop() {
	c.running = false
	close(c.stopCh)
}

// Get 获取配置项
func (c *ConfigClient) Get(key string) string {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.configs[key]
}

// GetAll 获取所有配置
func (c *ConfigClient) GetAll() map[string]string {
	c.mu.RLock()
	defer c.mu.RUnlock()
	result := make(map[string]string, len(c.configs))
	for k, v := range c.configs {
		result[k] = v
	}
	return result
}

// OnChange 注册配置变更回调
func (c *ConfigClient) OnChange(callback func(map[string]string)) {
	c.onChange = callback
}

func (c *ConfigClient) login() {
	payload := map[string]interface{}{
		"AppId":    c.AppID,
		"Secret":   c.Secret,
		"ClientId": c.ClientID,
		"AppName":  c.AppID,
	}

	body, err := json.Marshal(payload)
	if err != nil {
		fmt.Fprintf(os.Stderr, "[Stardust] Config login marshal failed: %v\n", err)
		return
	}

	resp, err := c.post(c.Server+"/App/Login", "application/json", body)
	if err != nil {
		fmt.Fprintf(os.Stderr, "[Stardust] Config login failed: %v\n", err)
		return
	}
	defer resp.Body.Close()

	var apiResp APIResponse
	if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil || apiResp.Code != 0 {
		return
	}

	var loginResp LoginResponse
	if err := json.Unmarshal(apiResp.Data, &loginResp); err != nil {
		return
	}

	c.token = loginResp.Token
	if loginResp.Expire > 0 {
		c.tokenExpire = time.Now().Unix() + int64(loginResp.Expire)
	}
}

func (c *ConfigClient) getAllConfig() {
	payload := map[string]interface{}{
		"AppId":    c.AppID,
		"Secret":   c.Secret,
		"ClientId": c.ClientID,
		"Version":  c.version,
	}

	body, err := json.Marshal(payload)
	if err != nil {
		fmt.Fprintf(os.Stderr, "[Stardust] Config marshal failed: %v\n", err)
		return
	}

	reqURL := fmt.Sprintf("%s/Config/GetAll?Token=%s", c.Server, url.QueryEscape(c.token))
	resp, err := c.post(reqURL, "application/json", body)
	if err != nil {
		fmt.Fprintf(os.Stderr, "[Stardust] Config GetAll failed: %v\n", err)
		return
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Fprintf(os.Stderr, "[Stardust] Config read response failed: %v\n", err)
		return
	}
	var apiResp APIResponse
	if err := json.Unmarshal(respBody, &apiResp); err != nil || apiResp.Code != 0 {
		return
	}

	var configInfo ConfigInfo
	if err := json.Unmarshal(apiResp.Data, &configInfo); err != nil {
		return
	}

	// 版本没变化,不更新
	if c.version > 0 && configInfo.Version == c.version && configInfo.Configs == nil {
		return
	}

	// 更新配置
	if configInfo.Configs != nil {
		c.mu.Lock()
		c.configs = configInfo.Configs
		c.version = configInfo.Version
		c.mu.Unlock()

		// 触发回调
		if c.onChange != nil {
			go c.onChange(configInfo.Configs)
		}
	}
}

// post 发送 SDK 自身请求,并标记为不参与自动埋点
func (c *ConfigClient) post(reqURL, contentType string, body []byte) (*http.Response, error) {
	req, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", contentType)
	req = req.WithContext(withSuppress(req.Context()))
	return c.client.Do(req)
}

func (c *ConfigClient) pollLoop() {
	// 首次立即获取配置
	c.getAllConfig()

	ticker := time.NewTicker(30 * time.Second)
	defer ticker.Stop()
	for {
		select {
		case <-ticker.C:
			c.getAllConfig()
		case <-c.stopCh:
			return
		}
	}
}