From de3dbe8521cb0e0f4a2c764cf90fc62109adc3ef Mon Sep 17 00:00:00 2001 From: Denozordec Date: Tue, 1 Sep 2026 15:09:19 +0700 Subject: [PATCH] =?UTF-8?q?fix(sync):=20=D0=BD=D0=B5=20=D1=81=D1=87=D0=B8?= =?UTF-8?q?=D1=82=D0=B0=D1=82=D1=8C=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81?= =?UTF-8?q?=20=D1=81=20=D0=BE=D0=B4=D0=BD=D0=B8=D0=BC=20IP=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=D0=B5=D1=80=D0=B2=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В payload для VPS Tracker lbMode не отдаём без пула уникальных IP; в UI показываем без резервирования вместо Round robin. Co-authored-by: Cursor --- apps/api/src/services/vps-tracker-sync.ts | 17 ++++++++- apps/api/test/vps-tracker-sync.test.ts | 30 +++++++++++++++ .../reui-kit/service-failover-panel.tsx | 9 ++++- .../components/services/service-unit-card.tsx | 37 ++++++++++++++++++- .../_auth/services/$serviceId/index.tsx | 8 +++- 5 files changed, 94 insertions(+), 7 deletions(-) diff --git a/apps/api/src/services/vps-tracker-sync.ts b/apps/api/src/services/vps-tracker-sync.ts index 2440bfa..6f5a333 100644 --- a/apps/api/src/services/vps-tracker-sync.ts +++ b/apps/api/src/services/vps-tracker-sync.ts @@ -3,6 +3,7 @@ import type { CfdmBindingSyncItem, LbMode, ServiceBindingView } from "@cfdm/shar import { isIpLiteral } from "@cfdm/shared"; import type { Db } from "@cfdm/db"; import { repos, getAppSettingsSecrets, touchVpsTrackerSync } from "@cfdm/db"; +import { isSharedPool } from "./routing/pool.js"; export function isLbMode(value: unknown): value is LbMode { return value === "round_robin" || value === "failover" || value === "weighted"; @@ -18,6 +19,16 @@ export function resolveLbModeForSync( return undefined; } +/** Effective HA only when the service has two or more unique origin IPs. */ +export function effectiveLbModeForSync( + bindingLbMode: string | undefined | null, + groupLbMode: string | undefined | null, + serviceIps: readonly string[], +): LbMode | undefined { + if (!isSharedPool(serviceIps)) return undefined; + return resolveLbModeForSync(bindingLbMode, groupLbMode); +} + function groupLbModeForService( db: Db, serviceId: number, @@ -166,6 +177,7 @@ export async function buildServiceSyncBindingsAsync( const items: CfdmBindingSyncItem[] = []; for (const binding of bindings) { const ips = await resolveBindingIpsForSync(binding, serviceIps, index, db); + const lbMode = effectiveLbModeForSync(binding.lb_mode, groupLb, serviceIps); items.push({ bindingId: binding.id, serviceId: service.id, @@ -176,7 +188,7 @@ export async function buildServiceSyncBindingsAsync( hostname: binding.hostname, ips, cnameTarget: cnameTargetForSync(binding), - lbMode: resolveLbModeForSync(binding.lb_mode, groupLb), + ...(lbMode ? { lbMode } : {}), }); } @@ -214,6 +226,7 @@ export async function buildAllSyncBindings( } const ips = await resolveBindingIpsForSync(binding, serviceIps, index, db); const groupLb = groupLbModeForService(db, binding.service_id, groupLbCache); + const lbMode = effectiveLbModeForSync(binding.lb_mode, groupLb, serviceIps); items.push({ bindingId: binding.id, serviceId: binding.service_id, @@ -224,7 +237,7 @@ export async function buildAllSyncBindings( hostname: binding.hostname, ips, cnameTarget: cnameTargetForSync(binding), - lbMode: resolveLbModeForSync(binding.lb_mode, groupLb), + ...(lbMode ? { lbMode } : {}), }); } return items; diff --git a/apps/api/test/vps-tracker-sync.test.ts b/apps/api/test/vps-tracker-sync.test.ts index 5509784..210fad1 100644 --- a/apps/api/test/vps-tracker-sync.test.ts +++ b/apps/api/test/vps-tracker-sync.test.ts @@ -6,6 +6,7 @@ import { import { resolveBindingIpsForSync, resolveLbModeForSync, + effectiveLbModeForSync, } from "../src/services/vps-tracker-sync.js"; function binding( @@ -183,6 +184,35 @@ describe("resolveLbModeForSync", () => { }); }); +describe("effectiveLbModeForSync", () => { + it("omits mode when unique origin IPs are below two", () => { + expect( + effectiveLbModeForSync("round_robin", "failover", ["203.0.113.10"]), + ).toBeUndefined(); + expect( + effectiveLbModeForSync("round_robin", "failover", [ + "203.0.113.10", + "203.0.113.10", + ]), + ).toBeUndefined(); + }); + + it("emits configured mode when the service has a pool", () => { + expect( + effectiveLbModeForSync("failover", "round_robin", [ + "203.0.113.10", + "203.0.113.20", + ]), + ).toBe("failover"); + expect( + effectiveLbModeForSync("off", "weighted", [ + "203.0.113.10", + "198.51.100.1", + ]), + ).toBe("weighted"); + }); +}); + describe("cfdmBindingSyncItemSchema lbMode", () => { const base = { bindingId: 1, diff --git a/apps/web/src/components/reui-kit/service-failover-panel.tsx b/apps/web/src/components/reui-kit/service-failover-panel.tsx index f4a70be..67bb691 100644 --- a/apps/web/src/components/reui-kit/service-failover-panel.tsx +++ b/apps/web/src/components/reui-kit/service-failover-panel.tsx @@ -75,18 +75,25 @@ function alertDescription(events: { kind: string; fqdns: string[] }[]): string { */ export function ServiceFailoverPanel({ lbMode = 'round_robin', + hasPool = true, ipHealth, bindings, history, probes = [], }: { lbMode?: LbMode + hasPool?: boolean ipHealth: readonly FailoverHealthInput[] bindings: readonly FailoverBindingPool[] history: readonly FailoverLogEntry[] probes?: readonly HealthLogProbe[] }) { - const copy = PANEL_COPY[lbMode] ?? PANEL_COPY.round_robin + const copy = hasPool + ? (PANEL_COPY[lbMode] ?? PANEL_COPY.round_robin) + : { + title: 'без резервирования', + description: 'Один origin IP — балансировка не применяется', + } const liveByIp = latestHealthByIp(probes) const overlayHealth = ipHealth.map((row) => { const live = liveByIp.get(row.ip) diff --git a/apps/web/src/components/services/service-unit-card.tsx b/apps/web/src/components/services/service-unit-card.tsx index 85de59d..e27b946 100644 --- a/apps/web/src/components/services/service-unit-card.tsx +++ b/apps/web/src/components/services/service-unit-card.tsx @@ -5,6 +5,7 @@ import { Repeat2Icon, ScaleIcon, ServerIcon, + UnplugIcon, type LucideIcon, } from 'lucide-react' @@ -21,6 +22,7 @@ import { ServiceIpList, } from '@/components/services/service-fqdn-list' import { serviceDisplayFqdn, serviceDisplayFqdns } from '@/lib/service-utils' +import { uniqueIpCount } from '@/lib/failover-events' import type { ServiceView } from '@/lib/schemas' import { Button } from '@cfdm/ui/components/button' import { @@ -75,7 +77,35 @@ const LB_MODE_META: Record< }, } -export function LbModeTile({ mode }: { mode: LbMode }) { +export function LbModeTile({ + mode, + hasPool = true, +}: { + mode: LbMode + hasPool?: boolean +}) { + if (!hasPool) { + return ( + + + + } + > + + без резервирования + + + ) + } + const meta = LB_MODE_META[mode] const Icon = meta.icon @@ -148,7 +178,10 @@ export function ServiceUnitCard({ {service.name} - + = 2} + />
diff --git a/apps/web/src/routes/_auth/services/$serviceId/index.tsx b/apps/web/src/routes/_auth/services/$serviceId/index.tsx index 324df0a..eb005d9 100644 --- a/apps/web/src/routes/_auth/services/$serviceId/index.tsx +++ b/apps/web/src/routes/_auth/services/$serviceId/index.tsx @@ -32,7 +32,7 @@ import { ServiceHealthMonitor, } from '@/components/reui-kit' import { api } from '@/lib/api-client' -import { hasSharedPool } from '@/lib/failover-events' +import { hasSharedPool, uniqueIpCount } from '@/lib/failover-events' import { enabledHealthProviders, providerHealthStatuses, @@ -268,7 +268,10 @@ function ServiceDetailPage() { description="Domain → Service → Node → Health → Failover" actions={ <> - + = 2} + /> = 2} ipHealth={service.ip_health} bindings={failoverBindings} history={failoverHistory}