fix(services): не балансировать сервис с одним IP
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / changes (push) Successful in 8s
quality / docker-check (push) Skipped
quality / web (push) Successful in 55s
quality / api (push) Successful in 52s
CD / quality (push) Successful in 2m4s
CD / publish (push) Successful in 1m51s
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / changes (push) Successful in 8s
quality / docker-check (push) Skipped
quality / web (push) Successful in 55s
quality / api (push) Successful in 52s
CD / quality (push) Successful in 2m4s
CD / publish (push) Successful in 1m51s
KPI и карточка показывают живой OK, а не гистерезис unknown; DNS по-прежнему ждёт повторные успехи. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -2341,6 +2341,113 @@ export function listIpHealthByServiceIds(
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseIpHealthState(status: string | null): IpHealthState | undefined {
|
||||
if (
|
||||
status === "up" ||
|
||||
status === "down" ||
|
||||
status === "degraded" ||
|
||||
status === "unknown"
|
||||
) {
|
||||
return status;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function bestAliveIpState(statuses: readonly IpHealthState[]): IpHealthState {
|
||||
if (statuses.some((status) => status === "up")) return "up";
|
||||
if (statuses.some((status) => status === "degraded")) return "degraded";
|
||||
if (statuses.some((status) => status === "down")) return "down";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* Latest probe per service+IP+provider from health_probe_log, then any-up per IP.
|
||||
* Display overlay — hysteresis in ip_health_status is unchanged (DNS).
|
||||
*/
|
||||
export function listLatestLiveHealthByServiceIds(
|
||||
db: Db,
|
||||
serviceIds: number[],
|
||||
): Map<number, ServiceIpHealthRow[]> {
|
||||
const result = new Map<number, ServiceIpHealthRow[]>();
|
||||
if (serviceIds.length === 0) return result;
|
||||
const idList = sql.join(
|
||||
serviceIds.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
);
|
||||
const rows = db.all<{
|
||||
service_id: number;
|
||||
ip: string;
|
||||
provider: string | null;
|
||||
status: string | null;
|
||||
latency_ms: number | null;
|
||||
last_error: string | null;
|
||||
colo: string | null;
|
||||
checked_at: string | null;
|
||||
}>(sql`
|
||||
SELECT sb.service_id AS service_id,
|
||||
l.ip AS ip,
|
||||
l.provider AS provider,
|
||||
l.status AS status,
|
||||
l.latency_ms AS latency_ms,
|
||||
l.error AS last_error,
|
||||
l.colo AS colo,
|
||||
l.checked_at AS checked_at
|
||||
FROM health_probe_log l
|
||||
INNER JOIN service_bindings sb
|
||||
ON l.scope = 'binding' AND l.ref_id = sb.id
|
||||
INNER JOIN (
|
||||
SELECT sb2.service_id AS service_id,
|
||||
l2.ip AS ip,
|
||||
l2.provider AS provider,
|
||||
MAX(l2.id) AS max_id
|
||||
FROM health_probe_log l2
|
||||
INNER JOIN service_bindings sb2
|
||||
ON l2.scope = 'binding' AND l2.ref_id = sb2.id
|
||||
WHERE sb2.service_id IN (${idList})
|
||||
GROUP BY sb2.service_id, l2.ip, l2.provider
|
||||
) latest
|
||||
ON latest.max_id = l.id
|
||||
`);
|
||||
|
||||
const byServiceIp = new Map<
|
||||
string,
|
||||
{ serviceId: number; ip: string; probes: ServiceIpHealthRow[] }
|
||||
>();
|
||||
for (const row of rows) {
|
||||
const status = parseIpHealthState(row.status);
|
||||
if (!status) continue;
|
||||
const key = `${row.service_id}\0${row.ip}`;
|
||||
const probe: ServiceIpHealthRow = {
|
||||
ip: row.ip,
|
||||
status,
|
||||
latency_ms: row.latency_ms,
|
||||
last_checked_at: row.checked_at,
|
||||
last_error: row.last_error,
|
||||
provider: normalizeStatusProvider(row.provider),
|
||||
colo: row.colo,
|
||||
};
|
||||
const bucket = byServiceIp.get(key);
|
||||
if (bucket) bucket.probes.push(probe);
|
||||
else {
|
||||
byServiceIp.set(key, {
|
||||
serviceId: row.service_id,
|
||||
ip: row.ip,
|
||||
probes: [probe],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const { serviceId, ip, probes } of byServiceIp.values()) {
|
||||
const status = bestAliveIpState(probes.map((probe) => probe.status));
|
||||
const preferred =
|
||||
probes.find((probe) => probe.status === status) ?? probes[0]!;
|
||||
const list = result.get(serviceId) ?? [];
|
||||
list.push({ ...preferred, ip, status });
|
||||
result.set(serviceId, list);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function mergeHealthAggregates(
|
||||
parts: Array<HealthAggregate | undefined | null>,
|
||||
): HealthAggregate {
|
||||
|
||||
Reference in New Issue
Block a user