fix(services): снимать Down только с общего FQDN
Персональные A не крутить и не трогать при падении. Панель называется по режиму балансировки. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { LbMode } from "@cfdm/shared";
|
||||
import { failoverDesired } from "./failover.js";
|
||||
import { isSharedPool } from "./pool.js";
|
||||
import { roundRobinDesired } from "./round-robin.js";
|
||||
import type { LbIpRow, LbTargetConfig } from "./types.js";
|
||||
import { weightedDesired } from "./weighted.js";
|
||||
@@ -7,6 +8,7 @@ import { weightedDesired } from "./weighted.js";
|
||||
export type { LbIpRow, LbTargetConfig } from "./types.js";
|
||||
export { isHealthy } from "./health.js";
|
||||
export { withBindingLock } from "./binding-lock.js";
|
||||
export { isSharedPool, shouldRecordFailoverDnsDiff } from "./pool.js";
|
||||
export { WEIGHTED_DNS_TTL, WEIGHTED_SLOT_MS, weightedDesired } from "./weighted.js";
|
||||
|
||||
export function selectActiveIpsByMode(
|
||||
@@ -24,6 +26,21 @@ export function selectActiveIpsByMode(
|
||||
return roundRobinDesired(rows);
|
||||
}
|
||||
|
||||
export function resolveDesiredAIps(
|
||||
config: LbTargetConfig,
|
||||
rows: LbIpRow[],
|
||||
fallbackIps: readonly string[],
|
||||
nowMs = Date.now(),
|
||||
): string[] {
|
||||
const fallback = [...fallbackIps];
|
||||
if (!isSharedPool(fallback)) return fallback;
|
||||
if (config.lb_mode === "weighted" || config.health_check_enabled) {
|
||||
const activeIps = selectActiveIpsByMode(config, rows, nowMs);
|
||||
if (activeIps.length > 0) return activeIps;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function strategyLabel(mode: LbMode): string {
|
||||
if (mode === "failover") return "Failover";
|
||||
if (mode === "weighted") return "Weighted";
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { LbMode } from "@cfdm/shared";
|
||||
|
||||
/** Shared pool FQDN — two or more A targets. Dedicated extra-FQDN has one IP. */
|
||||
export function isSharedPool(ips: readonly string[]): boolean {
|
||||
return ips.length >= 2;
|
||||
}
|
||||
|
||||
export function shouldRecordFailoverDnsDiff(input: {
|
||||
configuredIps: readonly string[];
|
||||
lbMode: LbMode;
|
||||
added: readonly string[];
|
||||
removed: readonly string[];
|
||||
downIps: ReadonlySet<string>;
|
||||
}): boolean {
|
||||
if (!isSharedPool(input.configuredIps)) return false;
|
||||
if (input.lbMode !== "weighted") return true;
|
||||
return [...input.added, ...input.removed].some((ip) => input.downIps.has(ip));
|
||||
}
|
||||
@@ -30,7 +30,10 @@ import { syncServiceToVpsTracker } from "./vps-tracker-sync.js";
|
||||
import { fireEnsureHealthWorker, DEFAULT_HEALTH_FALLBACKS } from "./health/health-worker-deploy.js";
|
||||
import {
|
||||
isHealthy,
|
||||
isSharedPool,
|
||||
resolveDesiredAIps,
|
||||
selectActiveIpsByMode,
|
||||
shouldRecordFailoverDnsDiff,
|
||||
withBindingLock,
|
||||
WEIGHTED_DNS_TTL,
|
||||
type LbIpRow,
|
||||
@@ -38,12 +41,12 @@ import {
|
||||
} from "./routing/index.js";
|
||||
|
||||
export type { LbIpRow, LbTargetConfig };
|
||||
export { selectActiveIpsByMode };
|
||||
export { resolveDesiredAIps, selectActiveIpsByMode, shouldRecordFailoverDnsDiff };
|
||||
|
||||
const AUTO_DNS_TTL = 1;
|
||||
|
||||
function ttlForLbMode(mode: LbMode): number {
|
||||
return mode === "weighted" ? WEIGHTED_DNS_TTL : AUTO_DNS_TTL;
|
||||
function ttlForBinding(mode: LbMode, ipCount: number): number {
|
||||
return mode === "weighted" && ipCount >= 2 ? WEIGHTED_DNS_TTL : AUTO_DNS_TTL;
|
||||
}
|
||||
|
||||
export function failoverARecordDiff(
|
||||
@@ -72,6 +75,22 @@ function recordFailoverDnsDiff(
|
||||
const { added, removed } = failoverARecordDiff(existingA, desiredIps);
|
||||
if (added.length === 0 && removed.length === 0) return;
|
||||
const binding = repos.getBinding(db, bindingId);
|
||||
const configuredIps = repos.listBindingIps(db, bindingId);
|
||||
const { config, rows } = getBindingLbState(db, bindingId);
|
||||
const downIps = new Set(
|
||||
rows.filter((row) => row.health === "down").map((row) => row.ip),
|
||||
);
|
||||
if (
|
||||
!shouldRecordFailoverDnsDiff({
|
||||
configuredIps,
|
||||
lbMode: config.lb_mode,
|
||||
added,
|
||||
removed,
|
||||
downIps,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
repos.insertFailoverLog(db, {
|
||||
serviceId: binding.service_id,
|
||||
bindingId,
|
||||
@@ -271,33 +290,17 @@ function getGroupLbState(
|
||||
};
|
||||
}
|
||||
|
||||
function computeActiveIps(
|
||||
db: Db,
|
||||
scope: HealthCheckScope,
|
||||
refId: number,
|
||||
): string[] {
|
||||
const state =
|
||||
scope === "binding"
|
||||
? getBindingLbState(db, refId)
|
||||
: getGroupLbState(db, refId);
|
||||
return selectActiveIpsByMode(state.config, state.rows);
|
||||
}
|
||||
|
||||
function desiredAIps(
|
||||
db: Db,
|
||||
scope: HealthCheckScope,
|
||||
refId: number,
|
||||
fallbackIps: string[],
|
||||
): string[] {
|
||||
const config =
|
||||
const state =
|
||||
scope === "binding"
|
||||
? getBindingLbState(db, refId).config
|
||||
: getGroupLbState(db, refId).config;
|
||||
if (config.lb_mode === "weighted" || config.health_check_enabled) {
|
||||
const activeIps = computeActiveIps(db, scope, refId);
|
||||
if (activeIps.length > 0) return activeIps;
|
||||
}
|
||||
return fallbackIps;
|
||||
? getBindingLbState(db, refId)
|
||||
: getGroupLbState(db, refId);
|
||||
return resolveDesiredAIps(state.config, state.rows, fallbackIps);
|
||||
}
|
||||
|
||||
async function collectKnownZones(
|
||||
@@ -349,7 +352,7 @@ async function buildView(db: Db, serviceId: number): Promise<ServiceView> {
|
||||
const { config, rows } = getBindingLbState(db, binding.id);
|
||||
const bindingActiveIps = targetCname
|
||||
? []
|
||||
: selectActiveIpsByMode(config, rows);
|
||||
: resolveDesiredAIps(config, rows, targetIps);
|
||||
|
||||
return {
|
||||
binding_id: binding.id,
|
||||
@@ -638,6 +641,7 @@ async function syncBindingDns(
|
||||
}
|
||||
|
||||
const binding = repos.getBinding(db, bindingId);
|
||||
const configuredIps = repos.listBindingIps(db, bindingId);
|
||||
await syncBindingADns(
|
||||
db,
|
||||
cf,
|
||||
@@ -645,7 +649,7 @@ async function syncBindingDns(
|
||||
domainId,
|
||||
hostname,
|
||||
desiredIps,
|
||||
ttlForLbMode(binding.lb_mode),
|
||||
ttlForBinding(binding.lb_mode, configuredIps.length),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1208,7 +1212,7 @@ async function syncGroupDomainDns(
|
||||
domainId,
|
||||
hostname,
|
||||
desiredIps,
|
||||
ttlForLbMode(group.lb_mode),
|
||||
ttlForBinding(group.lb_mode, fallbackIps.length),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1702,6 +1706,7 @@ export async function reconcileDnsForTarget(
|
||||
if (cnameTarget) return;
|
||||
const ips = repos.listServiceIps(db, service.id);
|
||||
const targetIps = repos.listBindingIps(db, binding.id);
|
||||
if (!isSharedPool(targetIps)) return;
|
||||
validateTargetIpsInPool(targetIps, ips);
|
||||
const desiredIps = desiredAIps(db, "binding", refId, targetIps);
|
||||
await syncBindingDns(
|
||||
@@ -1735,6 +1740,7 @@ export async function reconcileWeightedDns(
|
||||
for (const binding of repos.listAllBindings(db)) {
|
||||
if (binding.lb_mode !== "weighted") continue;
|
||||
if (binding.cname_target?.trim()) continue;
|
||||
if (!isSharedPool(binding.target_ips ?? [])) continue;
|
||||
try {
|
||||
await withBindingLock(binding.id, async () => {
|
||||
const latest = repos.getBinding(db, binding.id);
|
||||
@@ -1743,7 +1749,7 @@ export async function reconcileWeightedDns(
|
||||
const service = repos.getService(db, latest.service_id);
|
||||
if (!shouldPushDns(db, service)) return;
|
||||
const targetIps = repos.listBindingIps(db, latest.id);
|
||||
if (targetIps.length === 0) return;
|
||||
if (!isSharedPool(targetIps)) return;
|
||||
const ips = repos.listServiceIps(db, service.id);
|
||||
validateTargetIpsInPool(targetIps, ips);
|
||||
const desiredIps = desiredAIps(db, "binding", latest.id, targetIps);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
resolveDesiredAIps,
|
||||
selectActiveIpsByMode,
|
||||
shouldRecordFailoverDnsDiff,
|
||||
type LbIpRow,
|
||||
type LbTargetConfig,
|
||||
} from "../src/services/service-config-service.js";
|
||||
@@ -144,3 +146,75 @@ describe("selectActiveIpsByMode", () => {
|
||||
expect(selectActiveIpsByMode(config, [])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDesiredAIps", () => {
|
||||
it("keeps a dedicated single IP even when down", () => {
|
||||
expect(
|
||||
resolveDesiredAIps(
|
||||
weightedConfig,
|
||||
[row("1.1.1.1", { health: "down" })],
|
||||
["1.1.1.1"],
|
||||
),
|
||||
).toEqual(["1.1.1.1"]);
|
||||
});
|
||||
|
||||
it("applies weighted overlay on a shared pool", () => {
|
||||
const rows = [
|
||||
row("1.1.1.1", { weight: 1, health: "up" }),
|
||||
row("2.2.2.2", { weight: 3, health: "up" }),
|
||||
];
|
||||
expect(
|
||||
resolveDesiredAIps(weightedConfig, rows, ["1.1.1.1", "2.2.2.2"], 0),
|
||||
).toEqual(selectActiveIpsByMode(weightedConfig, rows, 0));
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldRecordFailoverDnsDiff", () => {
|
||||
it("skips dedicated extra-FQDN", () => {
|
||||
expect(
|
||||
shouldRecordFailoverDnsDiff({
|
||||
configuredIps: ["1.1.1.1"],
|
||||
lbMode: "failover",
|
||||
added: [],
|
||||
removed: ["1.1.1.1"],
|
||||
downIps: new Set(["1.1.1.1"]),
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("skips weighted live-to-live slot swap", () => {
|
||||
expect(
|
||||
shouldRecordFailoverDnsDiff({
|
||||
configuredIps: ["1.1.1.1", "2.2.2.2"],
|
||||
lbMode: "weighted",
|
||||
added: ["2.2.2.2"],
|
||||
removed: ["1.1.1.1"],
|
||||
downIps: new Set(),
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("logs weighted swap when a down ip leaves the pool", () => {
|
||||
expect(
|
||||
shouldRecordFailoverDnsDiff({
|
||||
configuredIps: ["1.1.1.1", "2.2.2.2"],
|
||||
lbMode: "weighted",
|
||||
added: ["2.2.2.2"],
|
||||
removed: ["1.1.1.1"],
|
||||
downIps: new Set(["1.1.1.1"]),
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("logs failover diffs on a shared pool", () => {
|
||||
expect(
|
||||
shouldRecordFailoverDnsDiff({
|
||||
configuredIps: ["1.1.1.1", "2.2.2.2"],
|
||||
lbMode: "failover",
|
||||
added: ["2.2.2.2"],
|
||||
removed: ["1.1.1.1"],
|
||||
downIps: new Set(["1.1.1.1"]),
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user