From 5d7a4469ea0677e26d06ce4fef52cf9ed017e141 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 30 May 2026 10:27:10 +0800 Subject: [PATCH] =?UTF-8?q?[=E4=BC=98=E5=8C=96]=20=E5=A2=9E=E5=8A=A0=20noo?= =?UTF-8?q?p=20apply=20=E6=8A=A5=E5=91=8A=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E5=9C=A8=E9=85=8D=E7=BD=AE=E6=9C=AA=E5=8F=98?= =?UTF-8?q?=E6=9B=B4=E6=97=B6=E8=AE=B0=E5=BD=95=E5=BA=94=E7=94=A8=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openflare_agent/internal/sync/service.go | 43 ++++++++++ openflare_agent/internal/sync/service_test.go | 79 ++++++++++++++++++- openflare_server/router/api_phase2_test.go | 16 ++++ .../apply-logs/components/apply-logs-page.tsx | 12 +++ 4 files changed, 148 insertions(+), 2 deletions(-) diff --git a/openflare_agent/internal/sync/service.go b/openflare_agent/internal/sync/service.go index 487e6ca7..eeaa05d2 100644 --- a/openflare_agent/internal/sync/service.go +++ b/openflare_agent/internal/sync/service.go @@ -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 == "" { diff --git a/openflare_agent/internal/sync/service_test.go b/openflare_agent/internal/sync/service_test.go index 3accfc70..9cd3c64d 100644 --- a/openflare_agent/internal/sync/service_test.go +++ b/openflare_agent/internal/sync/service_test.go @@ -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{ diff --git a/openflare_server/router/api_phase2_test.go b/openflare_server/router/api_phase2_test.go index 0ec57c69..fa06306a 100644 --- a/openflare_server/router/api_phase2_test.go +++ b/openflare_server/router/api_phase2_test.go @@ -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", diff --git a/openflare_server/web/features/apply-logs/components/apply-logs-page.tsx b/openflare_server/web/features/apply-logs/components/apply-logs-page.tsx index 491510a6..98e03885 100644 --- a/openflare_server/web/features/apply-logs/components/apply-logs-page.tsx +++ b/openflare_server/web/features/apply-logs/components/apply-logs-page.tsx @@ -86,6 +86,18 @@ export function ApplyLogsPage() { const [customRetentionDays, setCustomRetentionDays] = useState('30'); const [feedback, setFeedback] = useState(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: () =>