fix(ui): не подменять неполные сетевые счётчики нулями
This commit is contained in:
1 parent
587dd94f82
commit
1deccf862c
13 files changed
+84
-99
No files matched your search
@@ -1,8 +1,10 @@
|
||||
import {boundedCompleteCount,completeCollectionCount} from "./complete-count.js"
|
||||
import {pollBgpCatalog} from "./bgp-catalog-poll.js"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { db } from "../db/index.js"
|
||||
import { servers } from "../db/schema.js"
|
||||
import type { BgpSessionRead } from "../types/server.js"
|
||||
import { parseBgpSessions } from "./bgp-parse-sessions.js"
|
||||
import { parseBgpSessions,parseBgpCatalogSessions } from "./bgp-parse-sessions.js"
|
||||
import { MikrotikClient } from "./mikrotik.js"
|
||||
|
||||
/** Как в UI `/alerts`: `serverName: peerName`. */
|
||||
@@ -29,15 +31,6 @@ export async function fetchBgpSessionsForAlerts(): Promise<BgpSessionRead[]> {
|
||||
return results.flat()
|
||||
}
|
||||
|
||||
export async function countBgpSessions(): Promise<number> {
|
||||
try {
|
||||
const result = await Promise.race([
|
||||
fetchBgpSessionsForAlerts(),
|
||||
new Promise<null>((resolve) => setTimeout(() => resolve(null), 8_000)),
|
||||
])
|
||||
if (!result) return 0
|
||||
return result.length
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
export async function countBgpSessions():Promise<number|undefined>{
|
||||
return boundedCompleteCount(async()=>{const enabled=await db.select().from(servers).where(eq(servers.enabled,true));return pollBgpCatalog(enabled,async server=>parseBgpCatalogSessions(server,await MikrotikClient.fromServer(server).getBgpSessions()))},value=>completeCollectionCount(value,"sessions"))
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import assert from "node:assert/strict"
|
||||
import {test} from "node:test"
|
||||
import {boundedCompleteCount,completeCollectionCount,completeContainerCount} from "./complete-count.js"
|
||||
test("complete counts distinguish known zero, partial and missing failure metadata",()=>{
|
||||
assert.equal(completeCollectionCount({rows:[],failures:[]},"rows"),0)
|
||||
for(const raw of [{rows:[]},{rows:[],failures:[{}]},{rows:null,failures:[]}])assert.equal(completeCollectionCount(raw,"rows"),undefined)
|
||||
assert.equal(completeContainerCount({servers:[]}),0);assert.equal(completeContainerCount({servers:[{availability:"available",containers:[{}],failures:[{resource:"envs"}]}]}),1)
|
||||
for(const availability of ["unsupported","error"])assert.equal(completeContainerCount({servers:[{availability,containers:[],failures:[]}]}),undefined)
|
||||
})
|
||||
test("bounded reader omits thrown, malformed and timed out counts without publishing late success",async()=>{
|
||||
assert.equal(await boundedCompleteCount(async()=>0,x=>x),0);assert.equal(await boundedCompleteCount(async()=>{throw Error("offline")},x=>x),undefined)
|
||||
for(const value of [-1,NaN,Infinity,1.5])assert.equal(await boundedCompleteCount(async()=>value,x=>x),undefined)
|
||||
let release!:(value:number)=>void;const gate=new Promise<number>(resolve=>release=resolve);assert.equal(await boundedCompleteCount(()=>gate,x=>x,5),undefined);release(9)
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
/** Unknown or partial results are omitted from the API, never represented as zero. */
|
||||
export async function boundedCompleteCount<T>(read:()=>Promise<T>,select:(value:T)=>number|undefined,timeoutMs=8000):Promise<number|undefined>{
|
||||
let timer:ReturnType<typeof setTimeout>|undefined
|
||||
try{return await Promise.race([Promise.resolve().then(read).then(select).then(value=>typeof value==="number"&&Number.isSafeInteger(value)&&value>=0?value:undefined).catch(()=>undefined),new Promise<undefined>(resolve=>{timer=setTimeout(()=>resolve(undefined),timeoutMs)})])}finally{if(timer!==undefined)clearTimeout(timer)}
|
||||
}
|
||||
export function completeCollectionCount(raw:unknown,key:string):number|undefined{
|
||||
if(typeof raw!=="object"||raw===null)return undefined
|
||||
const value=raw as Record<string,unknown>
|
||||
if(!Array.isArray(value.failures)||value.failures.length||!Array.isArray(value[key]))return undefined
|
||||
return value[key].length
|
||||
}
|
||||
export function completeContainerCount(raw:unknown):number|undefined{
|
||||
if(typeof raw!=="object"||raw===null||!("servers" in raw)||!Array.isArray(raw.servers))return undefined
|
||||
let count=0
|
||||
for(const row of raw.servers){if(typeof row!=="object"||row===null||row.availability!=="available"||!Array.isArray(row.containers)||!Array.isArray(row.failures)||row.failures.some((failure:unknown)=>typeof failure!=="object"||failure===null||!("resource" in failure)||failure.resource==="containers"))return undefined;count+=row.containers.length}
|
||||
return count
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import {boundedCompleteCount,completeContainerCount} from "./complete-count.js"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { db } from "../db/index.js"
|
||||
import { servers } from "../db/schema.js"
|
||||
@@ -171,18 +172,7 @@ export async function listContainersForServer(server: ServerRow): Promise<Contai
|
||||
}
|
||||
}
|
||||
|
||||
export async function countContainers(): Promise<number> {
|
||||
try {
|
||||
const result = await Promise.race([
|
||||
listContainers(),
|
||||
new Promise<null>((resolve) => setTimeout(() => resolve(null), 8_000)),
|
||||
])
|
||||
if (!result) return 0
|
||||
return result.length
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
export async function countContainers():Promise<number|undefined>{return boundedCompleteCount(()=>listContainersState(),completeContainerCount)}
|
||||
|
||||
function encodeRosId(rosId: string): string {
|
||||
return encodeURIComponent(rosId)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {boundedCompleteCount,completeCollectionCount} from "./complete-count.js"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { db } from "../db/index.js"
|
||||
import { servers } from "../db/schema.js"
|
||||
@@ -576,18 +577,7 @@ export async function listIpsec(opts?: { serverId?: string }): Promise<IpsecList
|
||||
return { servers: summaries, clients, failures }
|
||||
}
|
||||
|
||||
export async function countIpsecClients(): Promise<number> {
|
||||
try {
|
||||
const result = await Promise.race([
|
||||
listIpsec(),
|
||||
new Promise<null>((resolve) => setTimeout(() => resolve(null), 8_000)),
|
||||
])
|
||||
if (!result) return 0
|
||||
return result.clients.length
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
export async function countIpsecClients():Promise<number|undefined>{return boundedCompleteCount(()=>listIpsec(),value=>completeCollectionCount(value,"clients"))}
|
||||
|
||||
export async function getEnabledIpsecServerById(serverId: string | number) {
|
||||
const id = typeof serverId === "number" ? serverId : Number.parseInt(String(serverId), 10)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {boundedCompleteCount,completeCollectionCount} from "./complete-count.js"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { db } from "../db/index.js"
|
||||
import { servers } from "../db/schema.js"
|
||||
@@ -128,15 +129,4 @@ export async function listVxlanTunnelsForServer(server: ServerRow): Promise<Vxla
|
||||
return fetchVxlanForServer(server)
|
||||
}
|
||||
|
||||
export async function countVxlanTunnels(): Promise<number> {
|
||||
try {
|
||||
const result = await Promise.race([
|
||||
listVxlanTunnels(),
|
||||
new Promise<null>((resolve) => setTimeout(() => resolve(null), 8_000)),
|
||||
])
|
||||
if (!result) return 0
|
||||
return result.length
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
export async function countVxlanTunnels():Promise<number|undefined>{return boundedCompleteCount(()=>listVxlanResult(),value=>completeCollectionCount(value,"tunnels"))}
|
||||
@@ -1,3 +1,4 @@
|
||||
import {boundedCompleteCount,completeCollectionCount} from "./complete-count.js"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { db } from "../db/index.js"
|
||||
import { servers } from "../db/schema.js"
|
||||
@@ -279,18 +280,7 @@ export async function listWireGuardInterfaces(opts?: {
|
||||
return { interfaces: results.flat(), failures }
|
||||
}
|
||||
|
||||
export async function countWireGuardInterfaces(): Promise<number> {
|
||||
try {
|
||||
const result = await Promise.race([
|
||||
await listWireGuardInterfaces({ includePrivateKey: false }),
|
||||
new Promise<null>((resolve) => setTimeout(() => resolve(null), 8_000)),
|
||||
])
|
||||
if (!result) return 0
|
||||
return result.interfaces.length
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
export async function countWireGuardInterfaces():Promise<number|undefined>{return boundedCompleteCount(()=>listWireGuardInterfaces({ includePrivateKey: false }),value=>completeCollectionCount(value,"interfaces"))}
|
||||
|
||||
export async function getEnabledServerById(serverId: string | number) {
|
||||
const id = typeof serverId === "number" ? serverId : Number.parseInt(String(serverId), 10)
|
||||
|
||||
Reference in new issue
Block a user