fix(api): сохранять каталог пинг-проб атомарно

This commit is contained in:
Denozordec committed 2026-10-04 11:53:48 +07:00
1 parent 9c22fa8a68
commit 53c1bfbd3b
15 files changed
+243 -122

No files matched your search

@@ -0,0 +1,21 @@
import type { PingProbeWrite } from "@mmapp/contracts"
export type PingProbePatch = Omit<PingProbeWrite, "filter"> & { probeFilter: string; sortOrder: number; updatedAt: string }
export type PingProbeTransaction = {
listIds: () => Promise<string[]>
remove: (id: string) => Promise<void>
upsert: (row: PingProbePatch, createdAt: string) => Promise<void>
}
export type PingProbeStore = { transaction: (run: (tx: PingProbeTransaction) => Promise<void>) => Promise<void> }
export async function replacePingProbes(store: PingProbeStore, probes: PingProbeWrite[], now = new Date().toISOString()): Promise<void> {
await store.transaction(async tx => {
const nextIds = new Set(probes.map(row => row.id))
for (const id of await tx.listIds()) if (!nextIds.has(id)) await tx.remove(id)
for (const [sortOrder, row] of probes.entries()) {
const { filter, intervalSec, showOnDashboard, ...config } = row
await tx.upsert({ ...config, probeFilter: filter, sortOrder, updatedAt: now,
...(intervalSec === undefined ? {} : { intervalSec: intervalSec > 0 ? Math.max(5, intervalSec) : 0 }),
...(showOnDashboard === undefined ? {} : { showOnDashboard }),
}, now)
}
})
}
+15 -43
View File
@@ -1,4 +1,6 @@
import { and, asc, eq, gte, inArray, max, or } from "drizzle-orm"
import { replacePingProbes } from "./ping-probe-replacement.js"
import type { PingProbeWrite } from "@mmapp/contracts"
import { and, asc, eq, gte, inArray, max, or, sql } from "drizzle-orm"
import { db } from "../db/index.js"
import {
servers,
@@ -467,49 +469,19 @@ export async function updateProbeShowOnDashboard(probeId: string, showOnDashboar
return r.length > 0
}
export async function replaceProbes(rows: Array<{
id: string
srcServerId: number
srcInterface: string
name: string
target: string
probeFilter: string
enabled: boolean
showOnDashboard?: boolean
intervalSec?: number
}>) {
const now = new Date().toISOString()
const existing = await db.select({ id: uptimeProbes.id }).from(uptimeProbes)
const nextIds = new Set(rows.map((r) => r.id))
for (const p of existing) {
if (nextIds.has(p.id)) continue
await db.delete(uptimeProbes).where(eq(uptimeProbes.id, p.id))
}
for (let i = 0; i < rows.length; i += 1) {
const r = rows[i]
const intervalSec = r.intervalSec != null && r.intervalSec > 0 ? Math.max(5, Math.floor(r.intervalSec)) : 0
const patch = {
srcServerId: r.srcServerId,
srcInterface: r.srcInterface || "",
name: r.name,
target: r.target,
probeFilter: r.probeFilter || "—",
enabled: r.enabled,
showOnDashboard: r.showOnDashboard ?? false,
intervalSec,
sortOrder: i,
updatedAt: now,
}
const updated = await db.update(uptimeProbes).set(patch).where(eq(uptimeProbes.id, r.id)).returning({ id: uptimeProbes.id })
if (updated.length > 0) continue
await db.insert(uptimeProbes).values({
id: r.id,
...patch,
createdAt: now,
export async function replaceProbes(rows: PingProbeWrite[]) {
await replacePingProbes({ transaction: run => db.transaction(async tx => {
await tx.execute(sql`select pg_advisory_xact_lock(hashtext('uptime-ping-probes'))`)
await run({
listIds: async () => (await tx.select({ id: uptimeProbes.id }).from(uptimeProbes)).map(row => row.id),
remove: async id => { await tx.delete(uptimeProbes).where(eq(uptimeProbes.id, id)) },
upsert: async (row, createdAt) => {
const { id, ...patch } = row
await tx.insert(uptimeProbes).values({ id, ...patch, createdAt })
.onConflictDoUpdate({ target: uptimeProbes.id, set: patch })
},
})
}
}) }, rows)
}
export async function readProbeSamplesSince(sinceIso: string) {