mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 14:06:36 +08:00
[优化] 接口优化, 心跳请求返回规则摘要
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
"data_dir": "./data",
|
||||
"openresty_container_name": "atsflare-openresty",
|
||||
"openresty_docker_image": "openresty/openresty:alpine",
|
||||
"heartbeat_interval": 30000,
|
||||
"sync_interval": 30000,
|
||||
"heartbeat_interval": 10000,
|
||||
"request_timeout": 10000
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func main() {
|
||||
NginxCertDir: cfg.OpenrestyCertDir,
|
||||
},
|
||||
)
|
||||
log.Printf("agent config loaded: server=%s node=%s ip=%s heartbeat_interval=%s sync_interval=%s route_config=%s cert_dir=%s", cfg.ServerURL, cfg.NodeName, cfg.NodeIP, cfg.HeartbeatInterval, cfg.SyncInterval, cfg.RouteConfigPath, cfg.CertDir)
|
||||
log.Printf("agent config loaded: server=%s node=%s ip=%s heartbeat_interval=%s route_config=%s cert_dir=%s", cfg.ServerURL, cfg.NodeName, cfg.NodeIP, cfg.HeartbeatInterval, cfg.RouteConfigPath, cfg.CertDir)
|
||||
|
||||
client := httpclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration())
|
||||
stateStore := state.NewStore(cfg.StatePath)
|
||||
|
||||
@@ -14,13 +14,13 @@ import (
|
||||
|
||||
type HeartbeatService interface {
|
||||
Register(ctx context.Context, payload protocol.NodePayload) (*protocol.RegisterNodeResponse, error)
|
||||
Heartbeat(ctx context.Context, payload protocol.NodePayload) (*protocol.AgentSettings, error)
|
||||
Heartbeat(ctx context.Context, payload protocol.NodePayload) (*protocol.HeartbeatResult, error)
|
||||
SetToken(token string)
|
||||
}
|
||||
|
||||
type SyncService interface {
|
||||
SyncOnStartup(ctx context.Context) error
|
||||
SyncOnce(ctx context.Context) error
|
||||
SyncOnStartup(ctx context.Context, target *protocol.ActiveConfigMeta) error
|
||||
SyncOnce(ctx context.Context, target *protocol.ActiveConfigMeta) error
|
||||
}
|
||||
|
||||
type Updater interface {
|
||||
@@ -61,20 +61,24 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
}
|
||||
log.Printf("agent runner started: node_id=%s node=%s ip=%s", nodeID, r.Config.NodeName, r.Config.NodeIP)
|
||||
if r.hasAgentToken() {
|
||||
if err = r.SyncService.SyncOnStartup(ctx); err != nil {
|
||||
r.recordSyncError(err)
|
||||
log.Printf("agent startup sync failed: %v", err)
|
||||
} else {
|
||||
log.Printf("agent startup sync completed")
|
||||
}
|
||||
r.refreshOpenrestyHealth(ctx)
|
||||
settings, hbErr := r.HeartbeatService.Heartbeat(ctx, r.nodePayload(nodeID))
|
||||
heartbeatResult, hbErr := r.HeartbeatService.Heartbeat(ctx, r.nodePayload(nodeID))
|
||||
if hbErr != nil {
|
||||
log.Printf("agent startup heartbeat failed: %v", hbErr)
|
||||
} else {
|
||||
if heartbeatResult == nil {
|
||||
heartbeatResult = &protocol.HeartbeatResult{}
|
||||
}
|
||||
log.Printf("agent startup heartbeat succeeded: node_id=%s", nodeID)
|
||||
r.applySettings(settings)
|
||||
r.applySettings(heartbeatResult.AgentSettings)
|
||||
if err = r.SyncService.SyncOnStartup(ctx, heartbeatResult.ActiveConfig); err != nil {
|
||||
r.recordSyncError(err)
|
||||
log.Printf("agent startup sync failed: %v", err)
|
||||
} else {
|
||||
log.Printf("agent startup sync completed")
|
||||
}
|
||||
r.tryRestartOpenresty(ctx)
|
||||
r.tryAutoUpdate(ctx)
|
||||
}
|
||||
} else if err = r.tryRegister(ctx, &nodeID); err != nil {
|
||||
log.Printf("agent initial discovery register failed: %v", err)
|
||||
@@ -82,8 +86,6 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
|
||||
heartbeatTicker := time.NewTicker(r.Config.HeartbeatInterval.Duration())
|
||||
defer heartbeatTicker.Stop()
|
||||
syncTicker := time.NewTicker(r.Config.SyncInterval.Duration())
|
||||
defer syncTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -98,25 +100,23 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
continue
|
||||
}
|
||||
r.refreshOpenrestyHealth(ctx)
|
||||
settings, hbErr := r.HeartbeatService.Heartbeat(ctx, r.nodePayload(nodeID))
|
||||
heartbeatResult, hbErr := r.HeartbeatService.Heartbeat(ctx, r.nodePayload(nodeID))
|
||||
if hbErr != nil {
|
||||
log.Printf("agent heartbeat failed: %v", hbErr)
|
||||
} else {
|
||||
if changed := r.applySettings(settings); changed {
|
||||
if heartbeatResult == nil {
|
||||
heartbeatResult = &protocol.HeartbeatResult{}
|
||||
}
|
||||
if changed := r.applySettings(heartbeatResult.AgentSettings); changed {
|
||||
heartbeatTicker.Reset(r.Config.HeartbeatInterval.Duration())
|
||||
syncTicker.Reset(r.Config.SyncInterval.Duration())
|
||||
}
|
||||
if err = r.SyncService.SyncOnce(ctx, heartbeatResult.ActiveConfig); err != nil {
|
||||
r.recordSyncError(err)
|
||||
log.Printf("agent sync failed: %v", err)
|
||||
}
|
||||
r.tryRestartOpenresty(ctx)
|
||||
r.tryAutoUpdate(ctx)
|
||||
}
|
||||
case <-syncTicker.C:
|
||||
if !r.hasAgentToken() {
|
||||
continue
|
||||
}
|
||||
if err = r.SyncService.SyncOnce(ctx); err != nil {
|
||||
r.recordSyncError(err)
|
||||
log.Printf("agent sync failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,14 +138,6 @@ func (r *Runner) applySettings(settings *protocol.AgentSettings) bool {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if settings.SyncInterval > 0 {
|
||||
newInterval := config.MillisecondDuration(time.Duration(settings.SyncInterval) * time.Millisecond)
|
||||
if newInterval != r.Config.SyncInterval {
|
||||
log.Printf("agent sync interval updated: %s -> %s", r.Config.SyncInterval, newInterval)
|
||||
r.Config.SyncInterval = newInterval
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
r.autoUpdate = settings.AutoUpdate
|
||||
r.updateNow = settings.UpdateNow
|
||||
r.updateRepo = strings.TrimSpace(settings.UpdateRepo)
|
||||
@@ -226,12 +218,24 @@ func (r *Runner) tryRegister(ctx context.Context, nodeID *string) error {
|
||||
r.HeartbeatService.SetToken(response.AgentToken)
|
||||
*nodeID = response.NodeID
|
||||
log.Printf("agent discovery registration succeeded: node_id=%s", response.NodeID)
|
||||
if err = r.SyncService.SyncOnStartup(ctx); err != nil {
|
||||
r.refreshOpenrestyHealth(ctx)
|
||||
heartbeatResult, heartbeatErr := r.HeartbeatService.Heartbeat(ctx, r.nodePayload(*nodeID))
|
||||
if heartbeatErr != nil {
|
||||
log.Printf("agent post-register heartbeat failed: %v", heartbeatErr)
|
||||
return nil
|
||||
}
|
||||
if heartbeatResult == nil {
|
||||
heartbeatResult = &protocol.HeartbeatResult{}
|
||||
}
|
||||
r.applySettings(heartbeatResult.AgentSettings)
|
||||
if err = r.SyncService.SyncOnStartup(ctx, heartbeatResult.ActiveConfig); err != nil {
|
||||
r.recordSyncError(err)
|
||||
log.Printf("agent post-register startup sync failed: %v", err)
|
||||
} else {
|
||||
log.Printf("agent post-register startup sync completed")
|
||||
}
|
||||
r.tryRestartOpenresty(ctx)
|
||||
r.tryAutoUpdate(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ type fakeHeartbeatService struct {
|
||||
registerErr error
|
||||
registerResp *protocol.RegisterNodeResponse
|
||||
heartbeatErrs []error
|
||||
heartbeatSettings []*protocol.AgentSettings
|
||||
heartbeatResults []*protocol.HeartbeatResult
|
||||
heartbeatPayloads []protocol.NodePayload
|
||||
onHeartbeat func(int)
|
||||
lastToken string
|
||||
@@ -34,7 +34,7 @@ func (f *fakeHeartbeatService) Register(ctx context.Context, payload protocol.No
|
||||
return f.registerResp, f.registerErr
|
||||
}
|
||||
|
||||
func (f *fakeHeartbeatService) Heartbeat(ctx context.Context, payload protocol.NodePayload) (*protocol.AgentSettings, error) {
|
||||
func (f *fakeHeartbeatService) Heartbeat(ctx context.Context, payload protocol.NodePayload) (*protocol.HeartbeatResult, error) {
|
||||
f.mu.Lock()
|
||||
f.heartbeatCalls++
|
||||
callIndex := f.heartbeatCalls
|
||||
@@ -43,16 +43,16 @@ func (f *fakeHeartbeatService) Heartbeat(ctx context.Context, payload protocol.N
|
||||
if len(f.heartbeatErrs) >= callIndex {
|
||||
err = f.heartbeatErrs[callIndex-1]
|
||||
}
|
||||
var settings *protocol.AgentSettings
|
||||
if len(f.heartbeatSettings) >= callIndex {
|
||||
settings = f.heartbeatSettings[callIndex-1]
|
||||
var result *protocol.HeartbeatResult
|
||||
if len(f.heartbeatResults) >= callIndex {
|
||||
result = f.heartbeatResults[callIndex-1]
|
||||
}
|
||||
onHeartbeat := f.onHeartbeat
|
||||
f.mu.Unlock()
|
||||
if onHeartbeat != nil {
|
||||
onHeartbeat(callIndex)
|
||||
}
|
||||
return settings, err
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (f *fakeHeartbeatService) SetToken(token string) {
|
||||
@@ -94,14 +94,14 @@ func (f *fakeRuntimeManager) Restart(ctx context.Context) error {
|
||||
return f.restartErr
|
||||
}
|
||||
|
||||
func (f *fakeSyncService) SyncOnStartup(ctx context.Context) error {
|
||||
func (f *fakeSyncService) SyncOnStartup(ctx context.Context, target *protocol.ActiveConfigMeta) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.startupCalls++
|
||||
return f.startupErr
|
||||
}
|
||||
|
||||
func (f *fakeSyncService) SyncOnce(ctx context.Context) error {
|
||||
func (f *fakeSyncService) SyncOnce(ctx context.Context, target *protocol.ActiveConfigMeta) error {
|
||||
f.mu.Lock()
|
||||
f.syncOnceCalls++
|
||||
callIndex := f.syncOnceCalls
|
||||
@@ -119,6 +119,7 @@ func TestRunnerKeepsHeartbeatWhenStartupSyncFails(t *testing.T) {
|
||||
|
||||
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
||||
heartbeatService := &fakeHeartbeatService{
|
||||
heartbeatResults: []*protocol.HeartbeatResult{{}},
|
||||
onHeartbeat: func(callCount int) {
|
||||
if callCount >= 2 {
|
||||
cancel()
|
||||
@@ -136,7 +137,6 @@ func TestRunnerKeepsHeartbeatWhenStartupSyncFails(t *testing.T) {
|
||||
AgentVersion: config.AgentVersion,
|
||||
NginxVersion: "1.27.1.2",
|
||||
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
|
||||
SyncInterval: config.MillisecondDuration(20 * time.Millisecond),
|
||||
},
|
||||
StateStore: stateStore,
|
||||
HeartbeatService: heartbeatService,
|
||||
@@ -170,6 +170,9 @@ func TestRunnerDoesNotExitOnHeartbeatOrSyncError(t *testing.T) {
|
||||
heartbeatService := &fakeHeartbeatService{
|
||||
registerErr: errors.New("register timeout"),
|
||||
heartbeatErrs: []error{errors.New("heartbeat timeout")},
|
||||
heartbeatResults: []*protocol.HeartbeatResult{
|
||||
{},
|
||||
},
|
||||
}
|
||||
syncService := &fakeSyncService{
|
||||
syncOnceErr: errors.New("openresty reload failed"),
|
||||
@@ -187,7 +190,6 @@ func TestRunnerDoesNotExitOnHeartbeatOrSyncError(t *testing.T) {
|
||||
AgentVersion: config.AgentVersion,
|
||||
NginxVersion: "1.27.1.2",
|
||||
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
|
||||
SyncInterval: config.MillisecondDuration(10 * time.Millisecond),
|
||||
},
|
||||
StateStore: stateStore,
|
||||
HeartbeatService: heartbeatService,
|
||||
@@ -225,7 +227,9 @@ func TestRunnerReportsOpenrestyHealthAndExecutesRestart(t *testing.T) {
|
||||
t.Fatalf("failed to seed state: %v", err)
|
||||
}
|
||||
heartbeatService := &fakeHeartbeatService{
|
||||
heartbeatSettings: []*protocol.AgentSettings{{RestartOpenrestyNow: true}},
|
||||
heartbeatResults: []*protocol.HeartbeatResult{{
|
||||
AgentSettings: &protocol.AgentSettings{RestartOpenrestyNow: true},
|
||||
}},
|
||||
onHeartbeat: func(callCount int) {
|
||||
if callCount >= 1 {
|
||||
cancel()
|
||||
@@ -244,7 +248,6 @@ func TestRunnerReportsOpenrestyHealthAndExecutesRestart(t *testing.T) {
|
||||
AgentVersion: config.AgentVersion,
|
||||
NginxVersion: "1.27.1.2",
|
||||
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
|
||||
SyncInterval: config.MillisecondDuration(100 * time.Millisecond),
|
||||
},
|
||||
StateStore: stateStore,
|
||||
HeartbeatService: heartbeatService,
|
||||
@@ -289,6 +292,7 @@ func TestRunnerDiscoveryRegisterUpdatesTokenAndNodeID(t *testing.T) {
|
||||
AgentToken: "agent-token-issued",
|
||||
Name: "edge-01",
|
||||
},
|
||||
heartbeatResults: []*protocol.HeartbeatResult{{}},
|
||||
onHeartbeat: func(callCount int) {
|
||||
if callCount >= 1 {
|
||||
cancel()
|
||||
@@ -313,7 +317,6 @@ func TestRunnerDiscoveryRegisterUpdatesTokenAndNodeID(t *testing.T) {
|
||||
AgentVersion: config.AgentVersion,
|
||||
NginxVersion: "1.27.1.2",
|
||||
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
|
||||
SyncInterval: config.MillisecondDuration(20 * time.Millisecond),
|
||||
},
|
||||
StateStore: stateStore,
|
||||
HeartbeatService: heartbeatService,
|
||||
@@ -323,7 +326,6 @@ func TestRunnerDiscoveryRegisterUpdatesTokenAndNodeID(t *testing.T) {
|
||||
runner.Config.AgentVersion = config.AgentVersion
|
||||
runner.Config.NginxVersion = "1.27.1.2"
|
||||
runner.Config.HeartbeatInterval = config.MillisecondDuration(10 * time.Millisecond)
|
||||
runner.Config.SyncInterval = config.MillisecondDuration(20 * time.Millisecond)
|
||||
|
||||
err = runner.Run(ctx)
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
|
||||
@@ -38,7 +38,6 @@ type Config struct {
|
||||
OpenrestyCertDir string `json:"openresty_cert_dir"`
|
||||
StatePath string `json:"state_path"`
|
||||
HeartbeatInterval MillisecondDuration `json:"heartbeat_interval"`
|
||||
SyncInterval MillisecondDuration `json:"sync_interval"`
|
||||
RequestTimeout MillisecondDuration `json:"request_timeout"`
|
||||
configPath string
|
||||
}
|
||||
@@ -60,7 +59,6 @@ type configFile struct {
|
||||
OpenrestyCertDir string `json:"openresty_cert_dir"`
|
||||
StatePath string `json:"state_path"`
|
||||
HeartbeatInterval MillisecondDuration `json:"heartbeat_interval"`
|
||||
SyncInterval MillisecondDuration `json:"sync_interval"`
|
||||
RequestTimeout MillisecondDuration `json:"request_timeout"`
|
||||
}
|
||||
|
||||
@@ -90,7 +88,6 @@ func Load(path string) (*Config, error) {
|
||||
OpenrestyCertDir: file.OpenrestyCertDir,
|
||||
StatePath: file.StatePath,
|
||||
HeartbeatInterval: file.HeartbeatInterval,
|
||||
SyncInterval: file.SyncInterval,
|
||||
RequestTimeout: file.RequestTimeout,
|
||||
}
|
||||
cfg.configPath = path
|
||||
@@ -148,10 +145,7 @@ func applyDefaults(cfg *Config, baseDir string) {
|
||||
}
|
||||
}
|
||||
if cfg.HeartbeatInterval <= 0 {
|
||||
cfg.HeartbeatInterval = MillisecondDuration(30 * time.Second)
|
||||
}
|
||||
if cfg.SyncInterval <= 0 {
|
||||
cfg.SyncInterval = MillisecondDuration(30 * time.Second)
|
||||
cfg.HeartbeatInterval = MillisecondDuration(10 * time.Second)
|
||||
}
|
||||
if cfg.RequestTimeout <= 0 {
|
||||
cfg.RequestTimeout = MillisecondDuration(10 * time.Second)
|
||||
|
||||
@@ -142,7 +142,6 @@ func TestLoadUsesMillisecondsForIntervals(t *testing.T) {
|
||||
"node_name": "edge-01",
|
||||
"node_ip": "10.0.0.8",
|
||||
"heartbeat_interval": 30000,
|
||||
"sync_interval": 45000,
|
||||
"request_timeout": 1500,
|
||||
}
|
||||
data, err := json.Marshal(payload)
|
||||
@@ -161,9 +160,6 @@ func TestLoadUsesMillisecondsForIntervals(t *testing.T) {
|
||||
if cfg.HeartbeatInterval.Duration() != 30*time.Second {
|
||||
t.Fatalf("unexpected heartbeat interval: %s", cfg.HeartbeatInterval)
|
||||
}
|
||||
if cfg.SyncInterval.Duration() != 45*time.Second {
|
||||
t.Fatalf("unexpected sync interval: %s", cfg.SyncInterval)
|
||||
}
|
||||
if cfg.RequestTimeout.Duration() != 1500*time.Millisecond {
|
||||
t.Fatalf("unexpected request timeout: %s", cfg.RequestTimeout)
|
||||
}
|
||||
@@ -182,7 +178,6 @@ func TestSavePersistsMillisecondsAndOmitsRuntimeVersions(t *testing.T) {
|
||||
}
|
||||
cfg.NginxVersion = "1.27.1.2"
|
||||
cfg.HeartbeatInterval = MillisecondDuration(5 * time.Second)
|
||||
cfg.SyncInterval = MillisecondDuration(6 * time.Second)
|
||||
cfg.RequestTimeout = MillisecondDuration(7 * time.Second)
|
||||
|
||||
if err = cfg.Save(); err != nil {
|
||||
@@ -206,9 +201,6 @@ func TestSavePersistsMillisecondsAndOmitsRuntimeVersions(t *testing.T) {
|
||||
if decoded["heartbeat_interval"] != float64(5000) {
|
||||
t.Fatalf("unexpected heartbeat interval: %#v", decoded["heartbeat_interval"])
|
||||
}
|
||||
if decoded["sync_interval"] != float64(6000) {
|
||||
t.Fatalf("unexpected sync interval: %#v", decoded["sync_interval"])
|
||||
}
|
||||
if decoded["request_timeout"] != float64(7000) {
|
||||
t.Fatalf("unexpected request timeout: %#v", decoded["request_timeout"])
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
type Client interface {
|
||||
RegisterNode(ctx context.Context, payload protocol.NodePayload) (*protocol.RegisterNodeResponse, error)
|
||||
Heartbeat(ctx context.Context, payload protocol.NodePayload) (*protocol.AgentSettings, error)
|
||||
Heartbeat(ctx context.Context, payload protocol.NodePayload) (*protocol.HeartbeatResult, error)
|
||||
SetToken(token string)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func (s *Service) Register(ctx context.Context, payload protocol.NodePayload) (*
|
||||
return s.client.RegisterNode(ctx, payload)
|
||||
}
|
||||
|
||||
func (s *Service) Heartbeat(ctx context.Context, payload protocol.NodePayload) (*protocol.AgentSettings, error) {
|
||||
func (s *Service) Heartbeat(ctx context.Context, payload protocol.NodePayload) (*protocol.HeartbeatResult, error) {
|
||||
return s.client.Heartbeat(ctx, payload)
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ func (c *Client) RegisterNode(ctx context.Context, payload protocol.NodePayload)
|
||||
return &resp.Data, nil
|
||||
}
|
||||
|
||||
func (c *Client) Heartbeat(ctx context.Context, payload protocol.NodePayload) (*protocol.AgentSettings, error) {
|
||||
func (c *Client) Heartbeat(ctx context.Context, payload protocol.NodePayload) (*protocol.HeartbeatResult, error) {
|
||||
resp := protocol.HeartbeatAPIResponse{}
|
||||
if err := c.postJSON(ctx, "/api/agent/nodes/heartbeat", payload, &resp); err != nil {
|
||||
return nil, err
|
||||
@@ -50,7 +50,10 @@ func (c *Client) Heartbeat(ctx context.Context, payload protocol.NodePayload) (*
|
||||
if !resp.Success {
|
||||
return nil, errors.New(resp.Message)
|
||||
}
|
||||
return resp.AgentSettings, nil
|
||||
return &protocol.HeartbeatResult{
|
||||
AgentSettings: resp.AgentSettings,
|
||||
ActiveConfig: resp.ActiveConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetActiveConfig(ctx context.Context) (*protocol.ActiveConfigResponse, error) {
|
||||
|
||||
@@ -7,15 +7,20 @@ type APIResponse[T any] struct {
|
||||
}
|
||||
|
||||
type HeartbeatAPIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data"`
|
||||
AgentSettings *AgentSettings `json:"agent_settings,omitempty"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data"`
|
||||
AgentSettings *AgentSettings `json:"agent_settings,omitempty"`
|
||||
ActiveConfig *ActiveConfigMeta `json:"active_config,omitempty"`
|
||||
}
|
||||
|
||||
type HeartbeatResult struct {
|
||||
AgentSettings *AgentSettings
|
||||
ActiveConfig *ActiveConfigMeta
|
||||
}
|
||||
|
||||
type AgentSettings struct {
|
||||
HeartbeatInterval int `json:"heartbeat_interval"`
|
||||
SyncInterval int `json:"sync_interval"`
|
||||
AutoUpdate bool `json:"auto_update"`
|
||||
UpdateRepo string `json:"update_repo"`
|
||||
UpdateNow bool `json:"update_now"`
|
||||
@@ -69,6 +74,11 @@ type ActiveConfigResponse struct {
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type ActiveConfigMeta struct {
|
||||
Version string `json:"version"`
|
||||
Checksum string `json:"checksum"`
|
||||
}
|
||||
|
||||
type SupportFile struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"atsflare-agent/internal/protocol"
|
||||
"atsflare-agent/internal/state"
|
||||
@@ -40,15 +41,15 @@ func New(client ConfigClient, nginxManager NginxManager, stateStore *state.Store
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) SyncOnce(ctx context.Context) error {
|
||||
return s.sync(ctx, false)
|
||||
func (s *Service) SyncOnce(ctx context.Context, target *protocol.ActiveConfigMeta) error {
|
||||
return s.sync(ctx, false, target)
|
||||
}
|
||||
|
||||
func (s *Service) SyncOnStartup(ctx context.Context) error {
|
||||
return s.sync(ctx, true)
|
||||
func (s *Service) SyncOnStartup(ctx context.Context, target *protocol.ActiveConfigMeta) error {
|
||||
return s.sync(ctx, true, target)
|
||||
}
|
||||
|
||||
func (s *Service) sync(ctx context.Context, startup bool) error {
|
||||
func (s *Service) sync(ctx context.Context, startup bool, target *protocol.ActiveConfigMeta) error {
|
||||
mode := "periodic"
|
||||
if startup {
|
||||
mode = "startup"
|
||||
@@ -57,20 +58,73 @@ func (s *Service) sync(ctx context.Context, startup bool) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentChecksum, err := s.nginxManager.CurrentChecksum()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if target != nil {
|
||||
target.Version = strings.TrimSpace(target.Version)
|
||||
target.Checksum = strings.TrimSpace(target.Checksum)
|
||||
}
|
||||
|
||||
if target == nil || target.Version == "" || target.Checksum == "" {
|
||||
if !startup {
|
||||
log.Printf("skipping sync because heartbeat returned no active config summary: mode=%s", mode)
|
||||
return nil
|
||||
}
|
||||
log.Printf("sync startup fallback: active config summary unavailable, fetching active config directly")
|
||||
config, fetchErr := s.client.GetActiveConfig(ctx)
|
||||
if fetchErr != nil {
|
||||
log.Printf("fetch active config failed: mode=%s error=%v", mode, fetchErr)
|
||||
return fetchErr
|
||||
}
|
||||
target = &protocol.ActiveConfigMeta{
|
||||
Version: config.Version,
|
||||
Checksum: config.Checksum,
|
||||
}
|
||||
return s.applyIfNeeded(ctx, mode, startup, snapshot, currentChecksum, target, config)
|
||||
}
|
||||
|
||||
if currentChecksum == target.Checksum {
|
||||
log.Printf("local openresty config already up to date: mode=%s version=%s", mode, target.Version)
|
||||
if startup {
|
||||
log.Printf("ensuring openresty runtime on startup: version=%s", target.Version)
|
||||
if err = s.nginxManager.EnsureRuntime(ctx, true); err != nil {
|
||||
snapshot.OpenrestyStatus = protocol.OpenrestyStatusUnhealthy
|
||||
snapshot.OpenrestyMessage = err.Error()
|
||||
_ = s.stateStore.Save(snapshot)
|
||||
return err
|
||||
}
|
||||
log.Printf("openresty runtime ensured on startup: version=%s", target.Version)
|
||||
snapshot.OpenrestyStatus = protocol.OpenrestyStatusHealthy
|
||||
snapshot.OpenrestyMessage = ""
|
||||
}
|
||||
snapshot.CurrentVersion = target.Version
|
||||
snapshot.CurrentChecksum = target.Checksum
|
||||
snapshot.LastError = ""
|
||||
log.Printf("sync finished without changes: mode=%s version=%s", mode, target.Version)
|
||||
return s.stateStore.Save(snapshot)
|
||||
}
|
||||
if snapshot.CurrentVersion == target.Version && snapshot.CurrentChecksum == target.Checksum && !startup {
|
||||
log.Printf("skipping config fetch because state already records target version/checksum: version=%s checksum=%s", target.Version, target.Checksum)
|
||||
return nil
|
||||
}
|
||||
|
||||
config, err := s.client.GetActiveConfig(ctx)
|
||||
if err != nil {
|
||||
log.Printf("fetch active config failed: mode=%s error=%v", mode, err)
|
||||
return err
|
||||
}
|
||||
currentChecksum, err := s.nginxManager.CurrentChecksum()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.applyIfNeeded(ctx, mode, startup, snapshot, currentChecksum, target, config)
|
||||
}
|
||||
|
||||
func (s *Service) applyIfNeeded(ctx context.Context, mode string, startup bool, snapshot *state.Snapshot, currentChecksum string, target *protocol.ActiveConfigMeta, config *protocol.ActiveConfigResponse) error {
|
||||
if currentChecksum == config.Checksum {
|
||||
log.Printf("local openresty config already up to date: mode=%s version=%s", mode, config.Version)
|
||||
if startup {
|
||||
log.Printf("ensuring openresty runtime on startup: version=%s", config.Version)
|
||||
if err = s.nginxManager.EnsureRuntime(ctx, true); err != nil {
|
||||
if err := s.nginxManager.EnsureRuntime(ctx, true); err != nil {
|
||||
snapshot.OpenrestyStatus = protocol.OpenrestyStatusUnhealthy
|
||||
snapshot.OpenrestyMessage = err.Error()
|
||||
_ = s.stateStore.Save(snapshot)
|
||||
@@ -86,6 +140,9 @@ func (s *Service) sync(ctx context.Context, startup bool) error {
|
||||
log.Printf("sync finished without changes: mode=%s version=%s", mode, config.Version)
|
||||
return s.stateStore.Save(snapshot)
|
||||
}
|
||||
if target != nil && (target.Version != config.Version || target.Checksum != config.Checksum) {
|
||||
log.Printf("active config changed between heartbeat and fetch: heartbeat_version=%s heartbeat_checksum=%s fetched_version=%s fetched_checksum=%s", target.Version, target.Checksum, config.Version, config.Checksum)
|
||||
}
|
||||
if snapshot.CurrentVersion == config.Version && snapshot.CurrentChecksum == config.Checksum && !startup {
|
||||
log.Printf("skipping apply because state already records target version/checksum: version=%s checksum=%s", config.Version, config.Checksum)
|
||||
return nil
|
||||
@@ -97,7 +154,7 @@ func (s *Service) sync(ctx context.Context, startup bool) error {
|
||||
mainConfigChecksum := checksumString(config.MainConfig)
|
||||
routeConfigChecksum := checksumString(routeConfig)
|
||||
log.Printf("applying new openresty config: mode=%s from_version=%s to_version=%s old_checksum=%s new_checksum=%s", mode, snapshot.CurrentVersion, config.Version, currentChecksum, config.Checksum)
|
||||
if err = s.nginxManager.Apply(ctx, config.MainConfig, routeConfig, config.SupportFiles); err != nil {
|
||||
if err := s.nginxManager.Apply(ctx, config.MainConfig, routeConfig, config.SupportFiles); err != nil {
|
||||
log.Printf("apply openresty config failed: mode=%s version=%s error=%v", mode, config.Version, err)
|
||||
snapshot.LastError = err.Error()
|
||||
snapshot.OpenrestyStatus = protocol.OpenrestyStatusUnhealthy
|
||||
@@ -126,10 +183,10 @@ func (s *Service) sync(ctx context.Context, startup bool) error {
|
||||
snapshot.LastError = ""
|
||||
snapshot.OpenrestyStatus = protocol.OpenrestyStatusHealthy
|
||||
snapshot.OpenrestyMessage = ""
|
||||
if err = s.stateStore.Save(snapshot); err != nil {
|
||||
if err := s.stateStore.Save(snapshot); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = s.client.ReportApplyLog(ctx, protocol.ApplyLogPayload{
|
||||
if err := s.client.ReportApplyLog(ctx, protocol.ApplyLogPayload{
|
||||
NodeID: snapshot.NodeID,
|
||||
Version: config.Version,
|
||||
Result: ApplyResultSuccess,
|
||||
|
||||
@@ -18,8 +18,9 @@ type fakeExecutor struct {
|
||||
}
|
||||
|
||||
type fakeClient struct {
|
||||
config protocol.ActiveConfigResponse
|
||||
reports []protocol.ApplyLogPayload
|
||||
config protocol.ActiveConfigResponse
|
||||
reports []protocol.ApplyLogPayload
|
||||
fetchCalls int
|
||||
}
|
||||
|
||||
type fakeManager struct {
|
||||
@@ -54,6 +55,7 @@ func (f *fakeExecutor) Restart(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (f *fakeClient) GetActiveConfig(ctx context.Context) (*protocol.ActiveConfigResponse, error) {
|
||||
f.fetchCalls++
|
||||
return &f.config, nil
|
||||
}
|
||||
|
||||
@@ -109,7 +111,10 @@ func TestSyncOnceSuccess(t *testing.T) {
|
||||
Executor: &fakeExecutor{},
|
||||
}, stateStore)
|
||||
|
||||
if err = service.SyncOnce(context.Background()); err != nil {
|
||||
if err = service.SyncOnce(context.Background(), &protocol.ActiveConfigMeta{
|
||||
Version: client.config.Version,
|
||||
Checksum: client.config.Checksum,
|
||||
}); err != nil {
|
||||
t.Fatalf("SyncOnce failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -192,7 +197,10 @@ func TestSyncOnceRollbackOnNginxFailure(t *testing.T) {
|
||||
},
|
||||
}, stateStore)
|
||||
|
||||
err = service.SyncOnce(context.Background())
|
||||
err = service.SyncOnce(context.Background(), &protocol.ActiveConfigMeta{
|
||||
Version: client.config.Version,
|
||||
Checksum: client.config.Checksum,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected SyncOnce to fail when nginx test fails")
|
||||
}
|
||||
@@ -255,7 +263,10 @@ func TestSyncOnStartupRecreatesRuntimeWhenChecksumMatches(t *testing.T) {
|
||||
|
||||
manager := &fakeManager{currentChecksum: "checksum-3"}
|
||||
service := New(client, manager, stateStore)
|
||||
if err = service.SyncOnStartup(context.Background()); err != nil {
|
||||
if err = service.SyncOnStartup(context.Background(), &protocol.ActiveConfigMeta{
|
||||
Version: client.config.Version,
|
||||
Checksum: client.config.Checksum,
|
||||
}); err != nil {
|
||||
t.Fatalf("SyncOnStartup failed: %v", err)
|
||||
}
|
||||
if len(manager.ensureCalls) != 1 || !manager.ensureCalls[0] {
|
||||
@@ -301,7 +312,10 @@ func TestSyncOnStartupRecordsRuntimeFailure(t *testing.T) {
|
||||
ensureErr: context.DeadlineExceeded,
|
||||
}
|
||||
service := New(client, manager, stateStore)
|
||||
if err = service.SyncOnStartup(context.Background()); err == nil {
|
||||
if err = service.SyncOnStartup(context.Background(), &protocol.ActiveConfigMeta{
|
||||
Version: client.config.Version,
|
||||
Checksum: client.config.Checksum,
|
||||
}); err == nil {
|
||||
t.Fatal("expected SyncOnStartup to fail when runtime recreation fails")
|
||||
}
|
||||
snapshot, err := stateStore.Load()
|
||||
@@ -315,3 +329,43 @@ func TestSyncOnStartupRecordsRuntimeFailure(t *testing.T) {
|
||||
t.Fatal("expected runtime error message to be recorded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncOnceSkipsFetchWhenHeartbeatChecksumMatches(t *testing.T) {
|
||||
client := &fakeClient{
|
||||
config: protocol.ActiveConfigResponse{
|
||||
Version: "20260309-005",
|
||||
Checksum: "checksum-5",
|
||||
MainConfig: "worker_processes auto;",
|
||||
RouteConfig: "server { listen 84; }",
|
||||
RenderedConfig: "server { listen 84; }",
|
||||
CreatedAt: time.Now().Format(time.RFC3339),
|
||||
},
|
||||
}
|
||||
stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json"))
|
||||
nodeID, err := stateStore.EnsureNodeID()
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureNodeID failed: %v", err)
|
||||
}
|
||||
if err = stateStore.Save(&state.Snapshot{
|
||||
NodeID: nodeID,
|
||||
CurrentVersion: client.config.Version,
|
||||
CurrentChecksum: client.config.Checksum,
|
||||
}); err != nil {
|
||||
t.Fatalf("failed to seed state: %v", err)
|
||||
}
|
||||
|
||||
manager := &fakeManager{currentChecksum: client.config.Checksum}
|
||||
service := New(client, manager, stateStore)
|
||||
if err = service.SyncOnce(context.Background(), &protocol.ActiveConfigMeta{
|
||||
Version: client.config.Version,
|
||||
Checksum: client.config.Checksum,
|
||||
}); err != nil {
|
||||
t.Fatalf("SyncOnce failed: %v", err)
|
||||
}
|
||||
if client.fetchCalls != 0 {
|
||||
t.Fatalf("expected no active config fetch when heartbeat checksum matches, got %d", client.fetchCalls)
|
||||
}
|
||||
if len(client.reports) != 0 {
|
||||
t.Fatal("expected no apply log when no config change is needed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,7 @@ var AgentDiscoveryToken = ""
|
||||
var NodeOfflineThreshold = 2 * time.Minute
|
||||
|
||||
// V3 operational settings (hot-reloadable via Option table)
|
||||
var AgentHeartbeatInterval = 30000 // milliseconds
|
||||
var AgentSyncInterval = 30000 // milliseconds
|
||||
var AgentHeartbeatInterval = 10000 // milliseconds
|
||||
var AgentUpdateRepo = "Rain-kl/ATSFlare"
|
||||
|
||||
// V5 OpenResty performance settings (hot-reloadable via Option table)
|
||||
|
||||
@@ -91,6 +91,7 @@ func AgentHeartbeat(c *gin.Context) {
|
||||
"message": "",
|
||||
"data": node.Node,
|
||||
"agent_settings": node.AgentSettings,
|
||||
"active_config": node.ActiveConfig,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ func InitOptionMap() {
|
||||
common.OptionMap["TurnstileSecretKey"] = ""
|
||||
common.OptionMap["AgentDiscoveryToken"] = ""
|
||||
common.OptionMap["AgentHeartbeatInterval"] = strconv.Itoa(common.AgentHeartbeatInterval)
|
||||
common.OptionMap["AgentSyncInterval"] = strconv.Itoa(common.AgentSyncInterval)
|
||||
common.OptionMap["NodeOfflineThreshold"] = strconv.Itoa(int(common.NodeOfflineThreshold.Milliseconds()))
|
||||
common.OptionMap["AgentUpdateRepo"] = common.AgentUpdateRepo
|
||||
common.OptionMap["OpenRestyWorkerProcesses"] = common.OpenRestyWorkerProcesses
|
||||
@@ -199,10 +198,6 @@ func updateOptionMap(key string, value string) {
|
||||
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||
common.AgentHeartbeatInterval = v
|
||||
}
|
||||
case "AgentSyncInterval":
|
||||
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||
common.AgentSyncInterval = v
|
||||
}
|
||||
case "NodeOfflineThreshold":
|
||||
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||
common.NodeOfflineThreshold = time.Duration(v) * time.Millisecond
|
||||
|
||||
@@ -284,9 +284,10 @@ func TestPhase2AgentLifecycle(t *testing.T) {
|
||||
t.Fatalf("unexpected heartbeat status %d: %s", restartHeartbeatRecorder.Code, restartHeartbeatRecorder.Body.String())
|
||||
}
|
||||
var restartHeartbeatBody struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
AgentSettings service.AgentSettings `json:"agent_settings"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
AgentSettings service.AgentSettings `json:"agent_settings"`
|
||||
ActiveConfig *service.ActiveConfigMeta `json:"active_config"`
|
||||
}
|
||||
if err = json.Unmarshal(restartHeartbeatRecorder.Body.Bytes(), &restartHeartbeatBody); err != nil {
|
||||
t.Fatalf("failed to decode heartbeat response: %v", err)
|
||||
@@ -297,6 +298,9 @@ func TestPhase2AgentLifecycle(t *testing.T) {
|
||||
if !restartHeartbeatBody.AgentSettings.RestartOpenrestyNow {
|
||||
t.Fatal("expected heartbeat response to instruct openresty restart")
|
||||
}
|
||||
if restartHeartbeatBody.ActiveConfig == nil || restartHeartbeatBody.ActiveConfig.Version == "" || restartHeartbeatBody.ActiveConfig.Checksum == "" {
|
||||
t.Fatal("expected heartbeat response to include active config summary")
|
||||
}
|
||||
|
||||
logsResp := performJSONRequest(t, engine, adminToken, http.MethodGet, "/api/apply-logs/?node_id="+createdNode.NodeID, nil)
|
||||
var logs []model.ApplyLog
|
||||
|
||||
@@ -57,7 +57,6 @@ type AgentConfigResponse struct {
|
||||
|
||||
type AgentSettings struct {
|
||||
HeartbeatInterval int `json:"heartbeat_interval"`
|
||||
SyncInterval int `json:"sync_interval"`
|
||||
AutoUpdate bool `json:"auto_update"`
|
||||
UpdateRepo string `json:"update_repo"`
|
||||
UpdateNow bool `json:"update_now"`
|
||||
@@ -66,9 +65,15 @@ type AgentSettings struct {
|
||||
RestartOpenrestyNow bool `json:"restart_openresty_now"`
|
||||
}
|
||||
|
||||
type ActiveConfigMeta struct {
|
||||
Version string `json:"version"`
|
||||
Checksum string `json:"checksum"`
|
||||
}
|
||||
|
||||
type HeartbeatResponse struct {
|
||||
Node *model.Node `json:"node"`
|
||||
AgentSettings *AgentSettings `json:"agent_settings"`
|
||||
Node *model.Node `json:"node"`
|
||||
AgentSettings *AgentSettings `json:"agent_settings"`
|
||||
ActiveConfig *ActiveConfigMeta `json:"active_config"`
|
||||
}
|
||||
|
||||
type NodeView struct {
|
||||
@@ -124,11 +129,14 @@ func HeartbeatNode(node *model.Node, payload AgentNodePayload) (*HeartbeatRespon
|
||||
if err := model.DB.Model(node).Select("ip", "agent_version", "nginx_version", "openresty_status", "openresty_message", "status", "current_version", "last_seen_at", "last_error", "update_requested", "update_channel", "update_tag", "restart_openresty_requested").Updates(node).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
activeConfig, err := GetActiveConfigMetaForAgent()
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
return &HeartbeatResponse{
|
||||
Node: node,
|
||||
AgentSettings: &AgentSettings{
|
||||
HeartbeatInterval: common.AgentHeartbeatInterval,
|
||||
SyncInterval: common.AgentSyncInterval,
|
||||
AutoUpdate: node.AutoUpdateEnabled,
|
||||
UpdateRepo: common.AgentUpdateRepo,
|
||||
UpdateNow: updateNow,
|
||||
@@ -136,6 +144,21 @@ func HeartbeatNode(node *model.Node, payload AgentNodePayload) (*HeartbeatRespon
|
||||
UpdateTag: updateTag,
|
||||
RestartOpenrestyNow: restartOpenrestyNow,
|
||||
},
|
||||
ActiveConfig: activeConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetActiveConfigMetaForAgent() (*ActiveConfigMeta, error) {
|
||||
version, err := model.GetActiveConfigVersion()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &ActiveConfigMeta{
|
||||
Version: version.Version,
|
||||
Checksum: version.Checksum,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,17 @@ func TestHeartbeatNodeReturnsPreviewUpdateSettings(t *testing.T) {
|
||||
if err := node.Insert(); err != nil {
|
||||
t.Fatalf("failed to seed node: %v", err)
|
||||
}
|
||||
if err := model.DB.Create(&model.ConfigVersion{
|
||||
Version: "20260313-001",
|
||||
SnapshotJSON: "{}",
|
||||
MainConfig: "worker_processes auto;",
|
||||
RenderedConfig: "server { listen 80; }",
|
||||
Checksum: "checksum-active-1",
|
||||
IsActive: true,
|
||||
CreatedBy: "root",
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("failed to seed active config version: %v", err)
|
||||
}
|
||||
|
||||
resp, err := HeartbeatNode(node, AgentNodePayload{
|
||||
NodeID: node.NodeID,
|
||||
@@ -94,6 +105,12 @@ func TestHeartbeatNodeReturnsPreviewUpdateSettings(t *testing.T) {
|
||||
if resp.AgentSettings == nil {
|
||||
t.Fatal("expected agent settings in heartbeat response")
|
||||
}
|
||||
if resp.ActiveConfig == nil {
|
||||
t.Fatal("expected active config summary in heartbeat response")
|
||||
}
|
||||
if resp.ActiveConfig.Version == "" || resp.ActiveConfig.Checksum == "" {
|
||||
t.Fatal("expected active config summary to include version and checksum")
|
||||
}
|
||||
if !resp.AgentSettings.UpdateNow {
|
||||
t.Fatal("expected update_now to be true")
|
||||
}
|
||||
|
||||
@@ -68,8 +68,7 @@ const defaultSystemFields = {
|
||||
};
|
||||
|
||||
const defaultOperationFields = {
|
||||
AgentHeartbeatInterval: '30000',
|
||||
AgentSyncInterval: '30000',
|
||||
AgentHeartbeatInterval: '10000',
|
||||
NodeOfflineThreshold: '120000',
|
||||
AgentUpdateRepo: 'Rain-kl/ATSFlare',
|
||||
OpenRestyWorkerProcesses: 'auto',
|
||||
@@ -335,8 +334,7 @@ export function SettingsPage() {
|
||||
});
|
||||
|
||||
setOperationFields({
|
||||
AgentHeartbeatInterval: optionMap.AgentHeartbeatInterval ?? '30000',
|
||||
AgentSyncInterval: optionMap.AgentSyncInterval ?? '30000',
|
||||
AgentHeartbeatInterval: optionMap.AgentHeartbeatInterval ?? '10000',
|
||||
NodeOfflineThreshold: optionMap.NodeOfflineThreshold ?? '120000',
|
||||
AgentUpdateRepo: optionMap.AgentUpdateRepo ?? 'Rain-kl/ATSFlare',
|
||||
OpenRestyWorkerProcesses: optionMap.OpenRestyWorkerProcesses ?? 'auto',
|
||||
@@ -942,10 +940,6 @@ export function SettingsPage() {
|
||||
operationFields.AgentHeartbeatInterval,
|
||||
10,
|
||||
);
|
||||
const sync = Number.parseInt(
|
||||
operationFields.AgentSyncInterval,
|
||||
10,
|
||||
);
|
||||
const offline = Number.parseInt(
|
||||
operationFields.NodeOfflineThreshold,
|
||||
10,
|
||||
@@ -954,9 +948,6 @@ export function SettingsPage() {
|
||||
if (Number.isNaN(heartbeat) || heartbeat < 5000) {
|
||||
throw new Error('心跳间隔不能小于 5000 毫秒。');
|
||||
}
|
||||
if (Number.isNaN(sync) || sync < 5000) {
|
||||
throw new Error('同步间隔不能小于 5000 毫秒。');
|
||||
}
|
||||
if (Number.isNaN(offline) || offline < 10000) {
|
||||
throw new Error('离线阈值不能小于 10000 毫秒。');
|
||||
}
|
||||
@@ -964,7 +955,6 @@ export function SettingsPage() {
|
||||
await saveOptionEntries(
|
||||
[
|
||||
['AgentHeartbeatInterval', String(heartbeat)],
|
||||
['AgentSyncInterval', String(sync)],
|
||||
['NodeOfflineThreshold', String(offline)],
|
||||
],
|
||||
'Agent 运行参数已保存。',
|
||||
@@ -980,7 +970,7 @@ export function SettingsPage() {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-5 md:grid-cols-3">
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
<ResourceField
|
||||
label={`心跳间隔 (${formatDurationLabel(operationFields.AgentHeartbeatInterval)})`}
|
||||
>
|
||||
@@ -995,20 +985,6 @@ export function SettingsPage() {
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField
|
||||
label={`同步间隔 (${formatDurationLabel(operationFields.AgentSyncInterval)})`}
|
||||
>
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={operationFields.AgentSyncInterval}
|
||||
onChange={(event) =>
|
||||
setOperationFields((previous) => ({
|
||||
...previous,
|
||||
AgentSyncInterval: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField
|
||||
label={`离线阈值 (${formatDurationLabel(operationFields.NodeOfflineThreshold)})`}
|
||||
>
|
||||
|
||||
@@ -422,7 +422,7 @@ export function WebsitesPage() {
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="网站"
|
||||
description="在同一页完成网站录入、默认证书绑定、证书导入和资产巡检,减少域名页与证书页之间的来回切换。"
|
||||
description="管理域名和TLS证书。"
|
||||
/>
|
||||
|
||||
{feedback ? (
|
||||
@@ -432,7 +432,6 @@ export function WebsitesPage() {
|
||||
<div className="grid gap-6 xl:grid-cols-[1.2fr_0.8fr]">
|
||||
<AppCard
|
||||
title={editingDomainId ? '编辑网站' : '快速添加网站'}
|
||||
description="录入域名后即可直接绑定默认证书。若证书尚未导入,可在右侧先导入再回到这里选择。"
|
||||
action={
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{editingDomainId ? (
|
||||
@@ -517,8 +516,7 @@ export function WebsitesPage() {
|
||||
</AppCard>
|
||||
|
||||
<AppCard
|
||||
title="证书快捷入口"
|
||||
description="先导入证书,再回到左侧立即绑定到网站。无需跳转到单独页面。"
|
||||
title="证书"
|
||||
action={
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<SecondaryButton
|
||||
|
||||
+14
-18
@@ -104,8 +104,7 @@ volumes:
|
||||
|
||||
| 配置项 | 作用 | 默认值 |
|
||||
| --- | --- | --- |
|
||||
| `AgentHeartbeatInterval` | Agent 心跳间隔(毫秒) | `30000` |
|
||||
| `AgentSyncInterval` | Agent 同步间隔(毫秒) | `30000` |
|
||||
| `AgentHeartbeatInterval` | Agent 心跳间隔(毫秒) | `10000` |
|
||||
| `NodeOfflineThreshold` | 节点离线判定阈值(毫秒) | `120000` |
|
||||
| `AgentUpdateRepo` | Agent 自更新仓库 | `Rain-kl/ATSFlare` |
|
||||
| `GlobalApiRateLimitNum` / `GlobalApiRateLimitDuration` | 全局 API 限流次数 / 时间窗口(秒) | `300` / `180` |
|
||||
@@ -216,13 +215,12 @@ go run ./cmd/agent -config ./agent.json
|
||||
{
|
||||
"server_url": "http://127.0.0.1:3000",
|
||||
"discovery_token": "replace-with-global-discovery-token",
|
||||
"data_dir": "./data",
|
||||
"openresty_container_name": "atsflare-openresty",
|
||||
"openresty_docker_image": "openresty/openresty:alpine",
|
||||
"heartbeat_interval": 30000,
|
||||
"sync_interval": 30000,
|
||||
"request_timeout": 10000
|
||||
}
|
||||
"data_dir": "./data",
|
||||
"openresty_container_name": "atsflare-openresty",
|
||||
"openresty_docker_image": "openresty/openresty:alpine",
|
||||
"heartbeat_interval": 10000,
|
||||
"request_timeout": 10000
|
||||
}
|
||||
```
|
||||
|
||||
使用节点专属 Token 的示例:
|
||||
@@ -239,11 +237,10 @@ go run ./cmd/agent -config ./agent.json
|
||||
"route_config_path": "/usr/local/openresty/nginx/conf/conf.d/atsflare_routes.conf",
|
||||
"cert_dir": "/usr/local/openresty/nginx/conf/certs",
|
||||
"openresty_cert_dir": "/usr/local/openresty/nginx/conf/certs",
|
||||
"state_path": "./data/agent-state.json",
|
||||
"heartbeat_interval": 30000,
|
||||
"sync_interval": 30000,
|
||||
"request_timeout": 10000
|
||||
}
|
||||
"state_path": "./data/agent-state.json",
|
||||
"heartbeat_interval": 10000,
|
||||
"request_timeout": 10000
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Agent 配置字段
|
||||
@@ -265,15 +262,14 @@ go run ./cmd/agent -config ./agent.json
|
||||
| `cert_dir` | Agent 在本机写入证书文件的目录 | 否 | 默认为 `data_dir` 下托管证书目录 | `./data/etc/nginx/certs` |
|
||||
| `openresty_cert_dir` | OpenResty 实际读取证书的目录 | 否 | 本机模式默认等于 `cert_dir`;Docker 模式默认 `/etc/nginx/atsflare-certs` | `/usr/local/openresty/nginx/conf/certs` |
|
||||
| `state_path` | Agent 本地状态文件路径 | 否 | 默认为 `data_dir` 下托管状态文件 | `./data/agent-state.json` |
|
||||
| `heartbeat_interval` | 心跳间隔 | 否 | `30000` 毫秒 | `30000` |
|
||||
| `sync_interval` | 配置同步间隔 | 否 | `30000` 毫秒 | `30000` |
|
||||
| `heartbeat_interval` | 心跳间隔 | 否 | `10000` 毫秒 | `10000` |
|
||||
| `request_timeout` | HTTP 请求超时时间 | 否 | `10000` 毫秒 | `10000` |
|
||||
|
||||
说明:
|
||||
|
||||
* `agent_token` 与 `discovery_token` 不能同时为空
|
||||
* `heartbeat_interval`、`sync_interval`、`request_timeout` 支持两种写法:
|
||||
* 毫秒整数,例如 `30000`
|
||||
* `heartbeat_interval`、`request_timeout` 支持两种写法:
|
||||
* 毫秒整数,例如 `10000`
|
||||
* Go duration 字符串,例如 `"30s"`
|
||||
* `node_name` 与 `node_ip` 未填写时会自动探测;若自动探测失败,配置校验会报错
|
||||
* 未配置 `openresty_path` 时,默认为 Docker OpenResty 模式
|
||||
|
||||
+18
-21
@@ -149,13 +149,12 @@ swag init -g main.go -o docs
|
||||
{
|
||||
"server_url": "http://127.0.0.1:3000",
|
||||
"agent_token": "replace-with-node-auth-token",
|
||||
"data_dir": "./data",
|
||||
"openresty_container_name": "atsflare-openresty",
|
||||
"openresty_docker_image": "openresty/openresty:alpine",
|
||||
"heartbeat_interval": 30000,
|
||||
"sync_interval": 30000,
|
||||
"request_timeout": 10000
|
||||
}
|
||||
"data_dir": "./data",
|
||||
"openresty_container_name": "atsflare-openresty",
|
||||
"openresty_docker_image": "openresty/openresty:alpine",
|
||||
"heartbeat_interval": 10000,
|
||||
"request_timeout": 10000
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 全局 `discovery_token`
|
||||
@@ -164,13 +163,12 @@ swag init -g main.go -o docs
|
||||
{
|
||||
"server_url": "http://127.0.0.1:3000",
|
||||
"discovery_token": "replace-with-global-discovery-token",
|
||||
"data_dir": "./data",
|
||||
"openresty_container_name": "atsflare-openresty",
|
||||
"openresty_docker_image": "openresty/openresty:alpine",
|
||||
"heartbeat_interval": 30000,
|
||||
"sync_interval": 30000,
|
||||
"request_timeout": 10000
|
||||
}
|
||||
"data_dir": "./data",
|
||||
"openresty_container_name": "atsflare-openresty",
|
||||
"openresty_docker_image": "openresty/openresty:alpine",
|
||||
"heartbeat_interval": 10000,
|
||||
"request_timeout": 10000
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
@@ -230,12 +228,12 @@ go build -o atsflare-agent ./cmd/agent
|
||||
|
||||
### 5.3 验证 Agent 应用
|
||||
|
||||
预期行为:
|
||||
|
||||
1. Agent 完成心跳与同步
|
||||
2. 自动注册模式下完成 Token 置换
|
||||
3. 拉取激活版本
|
||||
4. 写入主配置、路由配置与必要证书文件
|
||||
预期行为:
|
||||
|
||||
1. Agent 完成心跳
|
||||
2. 自动注册模式下完成 Token 置换
|
||||
3. 心跳响应返回激活版本摘要;若版本或 checksum 不一致,则拉取激活版本
|
||||
4. 写入主配置、路由配置与必要证书文件
|
||||
5. 执行 `openresty -t`
|
||||
6. 执行 `openresty -s reload`
|
||||
7. 上报应用结果
|
||||
@@ -448,7 +446,6 @@ GitHub Release 中的 Agent 二进制命名格式:
|
||||
| 参数 | 说明 | Agent 字段 |
|
||||
| ---------------------- | -------------------- | ------------------ |
|
||||
| AgentHeartbeatInterval | 心跳间隔(毫秒) | heartbeat_interval |
|
||||
| AgentSyncInterval | 同步间隔(毫秒) | sync_interval |
|
||||
| NodeOfflineThreshold | 节点离线阈值(毫秒) | - |
|
||||
| AgentUpdateRepo | 自动更新仓库 | update_repo |
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ ATSFlare Agent (register / heartbeat / sync / apply / update)
|
||||
* Agent 负责本地落盘、校验、reload、回滚、自更新,不负责维护独立于 Server 的主配置真相
|
||||
* 发布通过“生成完整版本并激活”完成
|
||||
* 历史版本不可变
|
||||
* Agent 常规轮询以 heartbeat 为主;heartbeat 响应返回当前激活版本的 `version` 与 `checksum` 摘要,Agent 仅在发现不一致时再拉取完整配置
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -220,6 +220,7 @@ Agent 必须满足:
|
||||
* 未显式配置 `node_name` 时自动获取主机名
|
||||
* 未显式配置 `node_ip` 时自动探测本机 IP
|
||||
* 周期性心跳与同步
|
||||
* 常规同步判定优先通过 heartbeat 响应中的激活版本 `version` / `checksum` 摘要完成;仅在 Agent 本地状态与摘要不一致时,再请求完整激活配置
|
||||
* 发现新版本时先备份旧文件
|
||||
* 写入新的主配置、路由配置与必要证书文件
|
||||
* 先执行 `openresty -t`
|
||||
|
||||
Reference in New Issue
Block a user