fix(api): сохранить неизвестный исход записи speed без ложной ошибки
This commit is contained in:
1 parent
c337fddac9
commit
f408afba20
12 files changed
+220
-51
No files matched your search
@@ -69,3 +69,16 @@ test("post-collection hook failure is reported separately from the recorded outc
|
||||
const receipt = await runRecordedSchedulerJob(h.deps)
|
||||
assert.equal(receipt.status, "ok"); assert.equal(h.records.length, 1); assert.equal(h.reported.length, 1)
|
||||
})
|
||||
|
||||
test("unconfirmed speed persistence is an execution limitation, not a successful job", async () => {
|
||||
const snapshot: SchedulerRunSnapshot = { v: 1, job: "uptime_speed", sampledAt: "now", runs: [{
|
||||
probeId: "probe", srcServerId: 1, srcServerName: "source", dstServerId: 2, dstServerName: "destination",
|
||||
srcInterface: "", dstInterface: "", protocol: "tcp", direction: "transmit", durationSec: 3,
|
||||
ok: false, error: "Сохранение результата не подтверждено; runId actual-run",
|
||||
}] }
|
||||
const h = harness(snapshot), receipt = await runRecordedSchedulerJob(h.deps)
|
||||
assert.equal(receipt.status, "error"); assert.match(receipt.error!, /runId actual-run/)
|
||||
assert.equal(h.events.some(event => event.eventType === "scheduler.job.ok"), false)
|
||||
assert.equal(h.records.length, 1)
|
||||
assert.equal(h.counts().collections, 1)
|
||||
})
|
||||
@@ -20,6 +20,9 @@ export function schedulerSnapshotError(snapshot: SchedulerRunSnapshot | null): s
|
||||
if (!snapshot) return null
|
||||
if ("fatalError" in snapshot && snapshot.fatalError) return snapshot.fatalError
|
||||
const errors = "errors" in snapshot ? [...(snapshot.errors ?? [])] : []
|
||||
if (snapshot.job === "uptime_speed") {
|
||||
for (const run of snapshot.runs) if (!run.ok && run.error) errors.push(run.error)
|
||||
}
|
||||
if (snapshot.job === "backups") {
|
||||
errors.push(...(snapshot.eventErrors ?? []))
|
||||
if (snapshot.metadataError && !errors.some(error => error.includes(snapshot.metadataError!))) errors.push("Метаданные: " + snapshot.metadataError)
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export interface SpeedProbeEndpoints { id: string; srcServerId: number; dstServerId: number }
|
||||
export class SpeedRunPersistenceUnknown extends Error {
|
||||
constructor(public readonly runId: string, cause?: unknown) {
|
||||
super(`Сетевой тест завершён, сохранение результата не подтверждено. Проверьте историю по runId ${runId} перед повтором`, { cause })
|
||||
}
|
||||
}
|
||||
|
||||
export interface SpeedProbeContext extends SpeedProbeEndpoints {
|
||||
srcInterface: string; dstInterface: string; protocol: "tcp" | "udp"; direction: "both" | "transmit" | "receive"; durationSec: number
|
||||
|
||||
@@ -8,7 +8,7 @@ import { withBtestNodeLocks } from "./btest-node-locks.js"
|
||||
import { MikrotikClient } from "./mikrotik.js"
|
||||
import { parsePing } from "./uptime-collector.js"
|
||||
import { assertBandwidthTestCompleted } from "./bandwidth-test-status.js"
|
||||
import { assertSpeedProbeContext, persistSpeedRun, type SpeedProbeContext } from "./speed-run-persistence.js"
|
||||
import { assertSpeedProbeContext, persistSpeedRun, SpeedRunPersistenceUnknown, type SpeedProbeContext } from "./speed-run-persistence.js"
|
||||
import { readBandwidthTestRates } from "./bandwidth-test-rates.js"
|
||||
|
||||
type SpeedWriteTransaction = Parameters<Parameters<typeof db.transaction>[0]>[0]
|
||||
@@ -143,49 +143,52 @@ export async function executeBandwidthSpeedTestInner(params: SpeedTestRunParams)
|
||||
}
|
||||
|
||||
const runId = String(params.runId ?? "").trim() || `sr-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||
await persistSpeedRun<SpeedWriteTransaction, SpeedProbeContext>(probeId ? target : null, {
|
||||
transaction: work => db.transaction(work),
|
||||
updateProbe: async (tx, target) => {
|
||||
const rows = await tx.update(uptimeSpeedProbes).set({
|
||||
lastRunAt: pingNow,
|
||||
lastTxAvgMbps: txFinal,
|
||||
lastRxAvgMbps: rxFinal,
|
||||
lastStatus: "done",
|
||||
lastError: "",
|
||||
lastPingRttMs: afterBtPing.rttMs,
|
||||
lastPingLossPct: afterBtPing.lossPct,
|
||||
lastPingAt: pingNow,
|
||||
lastPingError: afterBtPing.error ?? "",
|
||||
updatedAt: pingNow,
|
||||
}).where(speedProbeContextWhere(target)).returning({ id: uptimeSpeedProbes.id })
|
||||
return rows.length === 1
|
||||
},
|
||||
insertRun: async (tx, associatedProbeId) => {
|
||||
await tx.insert(uptimeSpeedTestRuns).values({
|
||||
id: runId,
|
||||
probeId: associatedProbeId,
|
||||
srcServerId: srcId,
|
||||
dstServerId: dstId,
|
||||
srcInterface: srcInterface || "",
|
||||
dstInterface: dstInterface || "",
|
||||
srcAddress: srcAddress || src.host,
|
||||
dstAddress: initialAddress,
|
||||
srcInterfaceAddress,
|
||||
dstInterfaceAddress,
|
||||
protocol,
|
||||
direction,
|
||||
durationSec,
|
||||
txAvgMbps: txFinal,
|
||||
rxAvgMbps: rxFinal,
|
||||
pingRttMs: afterBtPing.rttMs,
|
||||
pingLossPct: afterBtPing.lossPct,
|
||||
pingError: afterBtPing.error ?? null,
|
||||
status: "done",
|
||||
error: null,
|
||||
createdAt: pingNow,
|
||||
})
|
||||
},
|
||||
})
|
||||
try {
|
||||
await persistSpeedRun<SpeedWriteTransaction, SpeedProbeContext>(probeId ? target : null, {
|
||||
transaction: work => db.transaction(work),
|
||||
updateProbe: async (tx, target) => {
|
||||
const rows = await tx.update(uptimeSpeedProbes).set({
|
||||
lastRunAt: pingNow,
|
||||
lastTxAvgMbps: txFinal,
|
||||
lastRxAvgMbps: rxFinal,
|
||||
lastStatus: "done",
|
||||
lastError: "",
|
||||
lastPingRttMs: afterBtPing.rttMs,
|
||||
lastPingLossPct: afterBtPing.lossPct,
|
||||
lastPingAt: pingNow,
|
||||
lastPingError: afterBtPing.error ?? "",
|
||||
updatedAt: pingNow,
|
||||
}).where(speedProbeContextWhere(target)).returning({ id: uptimeSpeedProbes.id })
|
||||
return rows.length === 1
|
||||
},
|
||||
insertRun: async (tx, associatedProbeId) => {
|
||||
await tx.insert(uptimeSpeedTestRuns).values({
|
||||
id: runId,
|
||||
probeId: associatedProbeId,
|
||||
srcServerId: srcId,
|
||||
dstServerId: dstId,
|
||||
srcInterface: srcInterface || "",
|
||||
dstInterface: dstInterface || "",
|
||||
srcAddress: srcAddress || src.host,
|
||||
dstAddress: initialAddress,
|
||||
srcInterfaceAddress,
|
||||
dstInterfaceAddress,
|
||||
protocol,
|
||||
direction,
|
||||
durationSec,
|
||||
txAvgMbps: txFinal,
|
||||
rxAvgMbps: rxFinal,
|
||||
pingRttMs: afterBtPing.rttMs,
|
||||
pingLossPct: afterBtPing.lossPct,
|
||||
pingError: afterBtPing.error ?? null,
|
||||
status: "done",
|
||||
error: null,
|
||||
createdAt: pingNow,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
} catch (error) { throw new SpeedRunPersistenceUnknown(runId, error) }
|
||||
|
||||
return {
|
||||
createdAt: pingNow,
|
||||
@@ -325,7 +328,7 @@ export async function runScheduledSpeedProbesOnce(): Promise<SpeedScheduledRunSn
|
||||
})
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
await recordSpeedTestFailure({
|
||||
if (!(e instanceof SpeedRunPersistenceUnknown)) await recordSpeedTestFailure({
|
||||
probeId: p.id,
|
||||
srcServerId: p.srcServerId,
|
||||
dstServerId: p.dstServerId,
|
||||
|
||||
Reference in new issue
Block a user