mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
2f60329886
代码质量全量清理,零行为变化:golangci 扩展集 13 类 linter(gosec/modernize/perfsprint/canonicalheader/usestdlibvars/wastedassign/intrange/errorlint/forcetypeassert/recvcheck/exhaustive/unparam)全量修复,测试代码质量(testifylint/thelper/usetesting)25→0,frpc 进程生命周期真 bug(进程组击杀)、全仓 go test -race 6 类数据竞争(含 1 个生产竞争)、SPDX license 头补齐 131 文件、前端测试套件 next-intl 迁移后 44 失败→全绿、过期 swagger 文档重新生成、pnpm-workspace 构建审批。 Experiments: #2-#17, #18, #20, #21, #23 Metric: total_issues 108 → 8 (-92.6%)
256 lines
9.0 KiB
Go
256 lines
9.0 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package sync
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"github.com/Rain-kl/Wavelet/internal/apps/agent/nginx"
|
|
"github.com/Rain-kl/Wavelet/internal/apps/agent/protocol"
|
|
"github.com/Rain-kl/Wavelet/internal/apps/agent/state"
|
|
)
|
|
|
|
func syncMode(startup bool) string {
|
|
if startup {
|
|
return "startup"
|
|
}
|
|
return "periodic"
|
|
}
|
|
|
|
func normalizeSyncTarget(target *protocol.ActiveConfigMeta) {
|
|
if target == nil {
|
|
return
|
|
}
|
|
target.Version = strings.TrimSpace(target.Version)
|
|
target.Checksum = strings.TrimSpace(target.Checksum)
|
|
}
|
|
|
|
func (s *Service) loadSyncState() (*state.Snapshot, string, error) {
|
|
snapshot, err := s.stateStore.Load()
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
currentChecksum, err := s.nginxManager.CurrentChecksum()
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return snapshot, currentChecksum, nil
|
|
}
|
|
|
|
func (s *Service) syncWithoutTarget(ctx context.Context, mode string, startup bool, snapshot *state.Snapshot, currentChecksum string) error {
|
|
if !startup {
|
|
slog.Debug("skipping sync because heartbeat returned no active config summary", "mode", mode)
|
|
return nil
|
|
}
|
|
slog.Debug("sync startup fallback: active config summary unavailable, fetching active config directly")
|
|
config, fetchErr := s.client.GetActiveConfig(ctx)
|
|
if fetchErr != nil {
|
|
slog.Error("fetch active config failed", "mode", mode, "error", fetchErr)
|
|
return fetchErr
|
|
}
|
|
target := &protocol.ActiveConfigMeta{
|
|
Version: config.Version,
|
|
Checksum: config.Checksum,
|
|
}
|
|
return s.applyIfNeeded(ctx, mode, startup, snapshot, currentChecksum, target, config)
|
|
}
|
|
|
|
func (s *Service) syncMatchingChecksum(ctx context.Context, mode string, startup bool, snapshot *state.Snapshot, currentChecksum string, target *protocol.ActiveConfigMeta) error {
|
|
if startup {
|
|
config, fetchErr := s.client.GetActiveConfig(ctx)
|
|
if fetchErr != nil {
|
|
slog.Error("fetch active config failed", "mode", mode, "error", fetchErr)
|
|
return fetchErr
|
|
}
|
|
return s.applyIfNeeded(ctx, mode, startup, snapshot, currentChecksum, target, config)
|
|
}
|
|
return s.finishUpToDateSync(ctx, mode, snapshot, target)
|
|
}
|
|
|
|
func (s *Service) finishUpToDateSync(ctx context.Context, mode string, snapshot *state.Snapshot, target *protocol.ActiveConfigMeta) error {
|
|
if err := s.reconcilePages(ctx, mode, snapshot); err != nil {
|
|
return err
|
|
}
|
|
slog.Debug("local openresty config already up to date", "mode", mode, "version", target.Version)
|
|
if shouldReportNoopApply(snapshot, target.Version, target.Checksum) {
|
|
if err := s.reportNoopApply(ctx, snapshot.NodeID, target.Version, target.Checksum, "", "", 0); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
snapshot.CurrentVersion = target.Version
|
|
snapshot.CurrentChecksum = target.Checksum
|
|
clearBlockedTarget(snapshot)
|
|
snapshot.LastError = ""
|
|
slog.Debug("sync finished without changes", "mode", mode, "version", target.Version)
|
|
return s.stateStore.Save(snapshot)
|
|
}
|
|
|
|
func (s *Service) reconcilePages(ctx context.Context, mode string, snapshot *state.Snapshot) error {
|
|
if !pagesReconcileNeeded(snapshot) {
|
|
return nil
|
|
}
|
|
if pagesDiscoveryNeeded(snapshot) {
|
|
config, err := s.client.GetActiveConfig(ctx)
|
|
if err != nil {
|
|
slog.Error("fetch active config failed", "mode", mode, "error", err)
|
|
return err
|
|
}
|
|
return s.syncPagesDeployments(ctx, snapshot, config)
|
|
}
|
|
return s.syncPagesDeployments(ctx, snapshot, nil)
|
|
}
|
|
|
|
func (s *Service) syncMismatchedChecksum(ctx context.Context, mode string, startup bool, snapshot *state.Snapshot, currentChecksum string, target *protocol.ActiveConfigMeta) error {
|
|
if isBlockedTarget(snapshot, target.Version, target.Checksum) {
|
|
slog.Warn("skipping blocked config version after previous failed apply", "mode", mode, "version", target.Version, "checksum", target.Checksum)
|
|
if startup {
|
|
if err := s.ensureRuntimeForCurrentConfig(ctx, mode, snapshot, currentChecksum); err != nil {
|
|
return err
|
|
}
|
|
return s.stateStore.Save(snapshot)
|
|
}
|
|
return nil
|
|
}
|
|
if hasBlockedTarget(snapshot) {
|
|
clearBlockedTarget(snapshot)
|
|
}
|
|
if snapshot.CurrentVersion == target.Version && snapshot.CurrentChecksum == target.Checksum && !startup {
|
|
reconciled := pagesReconcileNeeded(snapshot)
|
|
if err := s.reconcilePages(ctx, mode, snapshot); err != nil {
|
|
return err
|
|
}
|
|
if !reconciled {
|
|
slog.Debug("skipping config fetch because state already records target version/checksum", "version", target.Version, "checksum", target.Checksum)
|
|
}
|
|
// Successful reconcile must clear sticky LastError so health events can resolve.
|
|
snapshot.LastError = ""
|
|
return s.stateStore.Save(snapshot)
|
|
}
|
|
|
|
config, err := s.client.GetActiveConfig(ctx)
|
|
if err != nil {
|
|
slog.Error("fetch active config failed", "mode", mode, "error", err)
|
|
return err
|
|
}
|
|
return s.applyIfNeeded(ctx, mode, startup, snapshot, currentChecksum, target, config)
|
|
}
|
|
|
|
func (s *Service) handleUpToDateConfig(ctx context.Context, mode string, snapshot *state.Snapshot, config *protocol.ActiveConfigResponse) error {
|
|
if err := s.syncPagesDeployments(ctx, snapshot, config); err != nil {
|
|
return err
|
|
}
|
|
slog.Debug("local openresty config already up to date", "mode", mode, "version", config.Version)
|
|
if shouldReportNoopApply(snapshot, config.Version, config.Checksum) {
|
|
rendered, renderErr := renderActiveConfig(config)
|
|
if renderErr != nil {
|
|
return renderErr
|
|
}
|
|
if err := s.reportNoopApply(
|
|
ctx,
|
|
snapshot.NodeID,
|
|
config.Version,
|
|
config.Checksum,
|
|
checksumString(rendered.mainConfig),
|
|
checksumString(rendered.routeConfig),
|
|
len(rendered.supportFiles),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
snapshot.CurrentVersion = config.Version
|
|
snapshot.CurrentChecksum = config.Checksum
|
|
clearBlockedTarget(snapshot)
|
|
snapshot.LastError = ""
|
|
slog.Debug("sync finished without changes", "mode", mode, "version", config.Version)
|
|
return s.stateStore.Save(snapshot)
|
|
}
|
|
|
|
func (s *Service) handleBlockedConfigAfterFetch(ctx context.Context, mode string, startup bool, snapshot *state.Snapshot, currentChecksum string, config *protocol.ActiveConfigResponse) (bool, error) {
|
|
if !isBlockedTarget(snapshot, config.Version, config.Checksum) {
|
|
return false, nil
|
|
}
|
|
slog.Warn("skipping blocked config after fetch because the same version previously failed", "mode", mode, "version", config.Version, "checksum", config.Checksum)
|
|
if startup {
|
|
if err := s.ensureRuntimeForCurrentConfig(ctx, mode, snapshot, currentChecksum); err != nil {
|
|
return true, err
|
|
}
|
|
return true, s.stateStore.Save(snapshot)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
type applyOutcomeResult struct {
|
|
reportResult string
|
|
message string
|
|
}
|
|
|
|
func normalizeApplyOutcome(outcome nginx.ApplyOutcome) (nginx.ApplyOutcome, string) {
|
|
message := strings.TrimSpace(outcome.Message)
|
|
if outcome.Status == "" {
|
|
outcome.Status = nginx.ApplyStatusFatal
|
|
if message == "" {
|
|
message = "openresty apply returned empty outcome"
|
|
}
|
|
}
|
|
return outcome, message
|
|
}
|
|
|
|
func updateSnapshotFromApplyOutcome(mode string, snapshot *state.Snapshot, config *protocol.ActiveConfigResponse, outcome nginx.ApplyOutcome, message string) applyOutcomeResult {
|
|
result := applyOutcomeResult{reportResult: ApplyResultFailed, message: message}
|
|
switch outcome.Status {
|
|
case nginx.ApplyStatusSuccess:
|
|
slog.Info("openresty config applied successfully", "mode", mode, "version", config.Version)
|
|
snapshot.CurrentVersion = config.Version
|
|
snapshot.CurrentChecksum = config.Checksum
|
|
clearBlockedTarget(snapshot)
|
|
snapshot.LastError = ""
|
|
snapshot.OpenrestyStatus = protocol.OpenrestyStatusHealthy
|
|
snapshot.OpenrestyMessage = ""
|
|
result.reportResult = ApplyResultSuccess
|
|
if result.message == "" {
|
|
result.message = "apply success"
|
|
}
|
|
case nginx.ApplyStatusWarning:
|
|
if result.message == "" {
|
|
result.message = "apply rolled back to previous config"
|
|
}
|
|
slog.Warn("openresty config apply rolled back", "mode", mode, "version", config.Version, "message", result.message)
|
|
markBlockedTarget(snapshot, config.Version, config.Checksum, result.message)
|
|
snapshot.LastError = result.message
|
|
snapshot.OpenrestyStatus = protocol.OpenrestyStatusHealthy
|
|
snapshot.OpenrestyMessage = result.message
|
|
result.reportResult = ApplyResultWarning
|
|
case nginx.ApplyStatusFatal:
|
|
// 致命错误与普通失败同走失败路径:标记阻塞并上报 Unhealthy。
|
|
fallthrough
|
|
default:
|
|
if result.message == "" {
|
|
result.message = "openresty apply failed"
|
|
}
|
|
slog.Error("apply openresty config failed", "mode", mode, "version", config.Version, "message", result.message)
|
|
markBlockedTarget(snapshot, config.Version, config.Checksum, result.message)
|
|
snapshot.LastError = result.message
|
|
snapshot.OpenrestyStatus = protocol.OpenrestyStatusUnhealthy
|
|
snapshot.OpenrestyMessage = result.message
|
|
}
|
|
return result
|
|
}
|
|
|
|
func snapshotMatchesTarget(snapshot *state.Snapshot, version string, checksum string) bool {
|
|
if snapshot == nil {
|
|
return false
|
|
}
|
|
return strings.TrimSpace(snapshot.CurrentVersion) == strings.TrimSpace(version) &&
|
|
strings.TrimSpace(snapshot.CurrentChecksum) == strings.TrimSpace(checksum)
|
|
}
|
|
|
|
func shouldReportApplyLog(alreadySynced bool, result string) bool {
|
|
if result != ApplyResultSuccess {
|
|
return true
|
|
}
|
|
return !alreadySynced
|
|
}
|