[优化] 增加 noop apply 报告逻辑,确保在配置未变更时记录应用日志

This commit is contained in:
ryan
2026-05-30 10:27:10 +08:00
parent 4e339caa9a
commit 5d7a4469ea
4 changed files with 148 additions and 2 deletions
+43
View File
@@ -92,6 +92,7 @@ func (s *Service) sync(ctx context.Context, startup bool, target *protocol.Activ
if currentChecksum == target.Checksum {
slog.Debug("local openresty config already up to date", "mode", mode, "version", target.Version)
shouldReport := shouldReportNoopApply(snapshot, target.Version, target.Checksum)
if startup {
slog.Debug("ensuring openresty runtime on startup", "version", target.Version)
if err = s.nginxManager.EnsureRuntime(ctx, true); err != nil {
@@ -104,6 +105,11 @@ func (s *Service) sync(ctx context.Context, startup bool, target *protocol.Activ
snapshot.OpenrestyStatus = protocol.OpenrestyStatusHealthy
snapshot.OpenrestyMessage = ""
}
if shouldReport {
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)
@@ -152,6 +158,7 @@ func (s *Service) ForceSyncOnce(ctx context.Context, target *protocol.ActiveConf
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 {
slog.Debug("local openresty config already up to date", "mode", mode, "version", config.Version)
shouldReport := shouldReportNoopApply(snapshot, config.Version, config.Checksum)
if startup {
slog.Debug("ensuring openresty runtime on startup", "version", config.Version)
if err := s.nginxManager.EnsureRuntime(ctx, true); err != nil {
@@ -164,6 +171,15 @@ func (s *Service) applyIfNeeded(ctx context.Context, mode string, startup bool,
snapshot.OpenrestyStatus = protocol.OpenrestyStatusHealthy
snapshot.OpenrestyMessage = ""
}
if shouldReport {
routeConfig := config.RouteConfig
if routeConfig == "" {
routeConfig = config.RenderedConfig
}
if err := s.reportNoopApply(ctx, snapshot.NodeID, config.Version, config.Checksum, checksumString(config.MainConfig), checksumString(routeConfig), len(config.SupportFiles)); err != nil {
return err
}
}
snapshot.CurrentVersion = config.Version
snapshot.CurrentChecksum = config.Checksum
clearBlockedTarget(snapshot)
@@ -266,6 +282,33 @@ func (s *Service) applyIfNeeded(ctx context.Context, mode string, startup bool,
return nil
}
func shouldReportNoopApply(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 (s *Service) reportNoopApply(ctx context.Context, nodeID string, version string, checksum string, mainConfigChecksum string, routeConfigChecksum string, supportFileCount int) error {
message := "local config already matches active version; apply skipped"
if err := s.client.ReportApplyLog(ctx, protocol.ApplyLogPayload{
NodeID: nodeID,
Version: strings.TrimSpace(version),
Result: ApplyResultSuccess,
Message: message,
Checksum: strings.TrimSpace(checksum),
MainConfigChecksum: strings.TrimSpace(mainConfigChecksum),
RouteConfigChecksum: strings.TrimSpace(routeConfigChecksum),
SupportFileCount: supportFileCount,
}); err != nil {
slog.Error("report noop apply log failed", "version", version, "error", err)
return err
}
slog.Debug("noop apply log reported", "version", version)
return nil
}
func outcomeError(version string, message string) error {
trimmed := strings.TrimSpace(message)
if trimmed == "" {
+77 -2
View File
@@ -323,8 +323,11 @@ func TestSyncOnStartupRecreatesRuntimeWhenChecksumMatches(t *testing.T) {
if len(manager.ensureCalls) != 1 || !manager.ensureCalls[0] {
t.Fatal("expected startup sync to recreate runtime")
}
if len(client.reports) != 0 {
t.Fatal("expected no apply report when checksum already matches")
if len(client.reports) != 1 || client.reports[0].Result != ApplyResultSuccess {
t.Fatal("expected startup sync to report noop success when state is refreshed")
}
if client.reports[0].Message != "local config already matches active version; apply skipped" {
t.Fatalf("unexpected noop apply message: %q", client.reports[0].Message)
}
snapshot, err := stateStore.Load()
if err != nil {
@@ -338,6 +341,78 @@ func TestSyncOnStartupRecreatesRuntimeWhenChecksumMatches(t *testing.T) {
}
}
func TestSyncOnceReportsNoopWhenVersionChangesButChecksumMatches(t *testing.T) {
client := &fakeClient{}
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: "20260309-002",
CurrentChecksum: "checksum-3",
}); err != nil {
t.Fatalf("failed to seed state: %v", err)
}
manager := &fakeManager{currentChecksum: "checksum-3"}
service := New(client, manager, stateStore)
if err = service.SyncOnce(context.Background(), &protocol.ActiveConfigMeta{
Version: "20260309-003",
Checksum: "checksum-3",
}); err != nil {
t.Fatalf("SyncOnce failed: %v", err)
}
if client.fetchCalls != 0 {
t.Fatalf("expected checksum match to skip config fetch, got %d", client.fetchCalls)
}
if len(manager.applyMainContents) != 0 {
t.Fatal("expected checksum match to skip apply")
}
if len(client.reports) != 1 || client.reports[0].Result != ApplyResultSuccess {
t.Fatalf("expected noop apply success report, got %+v", client.reports)
}
if client.reports[0].Version != "20260309-003" || client.reports[0].Checksum != "checksum-3" {
t.Fatalf("unexpected noop apply report: %+v", client.reports[0])
}
snapshot, err := stateStore.Load()
if err != nil {
t.Fatalf("failed to load state: %v", err)
}
if snapshot.CurrentVersion != "20260309-003" || snapshot.CurrentChecksum != "checksum-3" {
t.Fatalf("expected state to refresh active version, got %+v", snapshot)
}
}
func TestSyncOnceDoesNotRepeatNoopReportWhenStateAlreadyMatches(t *testing.T) {
client := &fakeClient{}
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: "20260309-003",
CurrentChecksum: "checksum-3",
}); err != nil {
t.Fatalf("failed to seed state: %v", err)
}
manager := &fakeManager{currentChecksum: "checksum-3"}
service := New(client, manager, stateStore)
if err = service.SyncOnce(context.Background(), &protocol.ActiveConfigMeta{
Version: "20260309-003",
Checksum: "checksum-3",
}); err != nil {
t.Fatalf("SyncOnce failed: %v", err)
}
if len(client.reports) != 0 {
t.Fatalf("expected matching state to skip duplicate noop report, got %+v", client.reports)
}
}
func TestSyncOnStartupRecordsRuntimeFailure(t *testing.T) {
client := &fakeClient{
config: protocol.ActiveConfigResponse{
@@ -631,6 +631,22 @@ func TestPhase2AgentLifecycle(t *testing.T) {
if logs.Total != 0 || len(logs.Rows) != 0 || logs.Current != 1 || logs.TotalPage != 0 {
t.Fatalf("expected empty apply log page after delete-all cleanup, got %+v", logs)
}
postDeleteApplyResp := performAgentJSONRequestWithToken(t, engine, createdNode.AgentToken, http.MethodPost, "/api/agent/apply-logs", map[string]any{
"version": activeConfig.Version,
"result": service.ApplyResultOK,
"message": "local config already matches active version; apply skipped",
"checksum": activeConfig.Checksum,
})
var postDeleteApplyLog model.ApplyLog
decodeResponseData(t, postDeleteApplyResp, &postDeleteApplyLog)
if postDeleteApplyLog.ID == 0 || postDeleteApplyLog.NodeID != createdNode.NodeID {
t.Fatalf("expected apply log to be recreated after delete-all cleanup, got %+v", postDeleteApplyLog)
}
postDeleteLogsResp := performJSONRequest(t, engine, adminToken, http.MethodGet, "/api/apply-logs/?node_id="+createdNode.NodeID, nil)
decodeResponseData(t, postDeleteLogsResp, &logs)
if logs.Total != 1 || len(logs.Rows) != 1 || logs.Rows[0].ID != postDeleteApplyLog.ID {
t.Fatalf("expected new apply log after delete-all cleanup, got %+v", logs)
}
updatedNodeResp := performJSONRequest(t, engine, adminToken, http.MethodPost, "/api/nodes/"+toString(createdNode.ID)+"/update", map[string]any{
"name": "shanghai-edge-1-renamed",
@@ -86,6 +86,18 @@ export function ApplyLogsPage() {
const [customRetentionDays, setCustomRetentionDays] = useState('30');
const [feedback, setFeedback] = useState<FeedbackState | null>(null);
useEffect(() => {
const nodeID = new URLSearchParams(window.location.search)
.get('node_id')
?.trim();
if (!nodeID) {
return;
}
setNodeFilterInput(nodeID);
setNodeFilter(nodeID);
setPageNo(1);
}, []);
const logsQuery = useQuery({
queryKey: applyLogsQueryKey(nodeFilter, pageNo, pageSize),
queryFn: () =>