fix(netflow): отклонять конкурентный сброс до удаления данных
This commit is contained in:
1 parent
a575795ff5
commit
32b7812169
9 files changed
+63
-5
No files matched your search
@@ -28,7 +28,7 @@
|
||||
"test:backups": "tsx src/services/backup-scheduler-run.test.ts && tsx src/services/backup-restore.test.ts && tsx src/services/backup-deletion.test.ts && tsx src/services/backup-dto.test.ts && tsx src/services/backup-storage-update.test.ts && tsx src/services/backup-job-execution.test.ts && tsx src/routes/backups-operations.test.ts && tsx src/services/s3-backup-client.test.ts",
|
||||
"test:live-maps": "tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts && tsx src/routes/bgp-catalog.test.ts && tsx src/services/bgp-catalog-parse.test.ts && tsx src/routes/ospf-catalog.test.ts && tsx src/services/ospf-observed-values.test.ts",
|
||||
"test": "npm run test:probes && npm run test:exec && npm run test:alert-engine && npm run test:auth && npm run test:wireguard && npm run test:traffic-rate && npm run test:traffic-flow && npm run test:users && npm run test:pg && npm run test:backups && npm run test:scheduler && npm run test:live-maps && npm run test:ospf-optimizer && npm run test:recursive-routes && npm run test:config-sync && npm run test:ros-file && npm run test:containers && npm run test:certificates && npm run test:netflow-settings && npm run test:netflow-purge",
|
||||
"test:netflow-purge": "tsx src/services/traffic-flow-purge-execution.test.ts && tsx src/routes/traffic-flow-purge.test.ts",
|
||||
"test:netflow-purge": "tsx src/db/operation-lock.test.ts && tsx src/services/traffic-flow-purge-execution.test.ts && tsx src/routes/traffic-flow-purge.test.ts",
|
||||
"test:netflow-settings": "tsx src/services/traffic-flow-key-execution.test.ts && tsx src/routes/traffic-flow-settings.test.ts && tsx src/plugins/auth.netflow-settings.test.ts",
|
||||
"test:geoip": "tsx src/services/traffic-flow-geoip.test.ts && tsx src/plugins/auth.geoip.test.ts",
|
||||
"test:ospf-optimizer": "tsx src/services/ospf-optimizer-identity.test.ts && tsx src/services/ospf-optimizer-metrics.test.ts && tsx src/services/ospf-optimizer-plan.test.ts && tsx src/routes/ospf-optimizer.test.ts",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import assert from "node:assert/strict"
|
||||
import {withTrafficFlowPurgeLock} from "./operation-lock.js"
|
||||
let held=false,runs=0,unlocks=0
|
||||
const releases:boolean[]=[]
|
||||
const connect=async()=>({query:async(sql:string,params:number[])=>{assert.deepEqual(params,[123]);if(sql.includes("pg_try_advisory_lock")){const acquired=!held;if(acquired)held=true;return {rows:[{acquired}]}}unlocks++;held=false;return {rows:[{released:true}]}},release:(destroy=false)=>{releases.push(destroy)}})
|
||||
let release!:()=>void
|
||||
const gate=new Promise<void>(resolve=>release=resolve)
|
||||
const first=withTrafficFlowPurgeLock(connect,123,async()=>{runs++;await gate;return "confirmed"})
|
||||
await new Promise<void>(resolve=>setImmediate(resolve))
|
||||
await assert.rejects(withTrafficFlowPurgeLock(connect,123,async()=>{runs++;return "unexpected"}),/уже выполняется/)
|
||||
assert.equal(runs,1);assert.equal(unlocks,0);assert.equal(held,true)
|
||||
release();assert.equal(await first,"confirmed");assert.equal(unlocks,1);assert.deepEqual(releases,[false,false])
|
||||
await assert.rejects(withTrafficFlowPurgeLock(connect,123,async()=>{throw Error("operation unknown")}),/operation unknown/);assert.equal(held,false);assert.equal(unlocks,2)
|
||||
for(const unlock of [false,"throws"]){let destroyed=false;const result=await withTrafficFlowPurgeLock(async()=>({query:async(sql:string)=>{if(sql.includes("pg_try"))return {rows:[{acquired:true}]};if(unlock==="throws")throw Error("unlock unavailable");return {rows:[{released:false}]}},release:(value=false)=>{destroyed=value}}),123,async()=>"committed");assert.equal(result,"committed");assert.equal(destroyed,true)}
|
||||
let released=false,discarded=false
|
||||
await assert.rejects(withTrafficFlowPurgeLock(async()=>({query:async()=>({rows:[{}]}),release:(destroy=false)=>{released=true;discarded=destroy}}),123,async()=>{throw Error("must not run")}),/не подтверждена/);assert.equal(released,true);assert.equal(discarded,true)
|
||||
console.log("NetFlow purge lock: concurrent request rejected before handler, unknown release destroys connection, operation receipt preserved")
|
||||
@@ -0,0 +1,26 @@
|
||||
type Client = {
|
||||
query: (sql: string, params: number[]) => Promise<{rows: Record<string, unknown>[]}>
|
||||
release: (destroy?: boolean) => void
|
||||
}
|
||||
/** Nonblocking session lock: a second destructive operation is rejected rather than queued. */
|
||||
export async function withTrafficFlowPurgeLock<T>(connect: () => Promise<Client>, key: number, run: () => Promise<T>): Promise<T> {
|
||||
const client = await connect()
|
||||
let owned = false, destroy = false
|
||||
try {
|
||||
destroy = true
|
||||
const result = await client.query("SELECT pg_try_advisory_lock($1) AS acquired", [key])
|
||||
if (result.rows[0]?.acquired === false) { destroy = false; throw new Error("Сброс NetFlow уже выполняется") }
|
||||
if (result.rows[0]?.acquired !== true) throw new Error("Блокировка сброса NetFlow не подтверждена")
|
||||
owned = true; destroy = false
|
||||
return await run()
|
||||
} finally {
|
||||
if (owned) {
|
||||
try {
|
||||
const result = await client.query("SELECT pg_advisory_unlock($1) AS released", [key])
|
||||
destroy = result.rows[0]?.released !== true
|
||||
} catch { destroy = true }
|
||||
}
|
||||
// A connection whose unlock failed must not return a held session lock to the pool.
|
||||
client.release(destroy)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { deleteTrafficFlowData, executeTrafficFlowPurge } from "./traffic-flow-purge-execution.js"
|
||||
import { withTrafficFlowPurgeLock } from "../db/operation-lock.js"
|
||||
import { Worker } from "node:worker_threads"
|
||||
import { db, dbAll, pool, withAdvisoryLock } from "../db/index.js"
|
||||
import { db, dbAll, pool } from "../db/index.js"
|
||||
import { dropExpiredPartitions } from "../db/partitions.js"
|
||||
import { servers } from "../db/schema.js"
|
||||
import type { FlowPurgeDto, FlowStatsDto, FlowTalkerDto } from "@mmapp/contracts/traffic-flow"
|
||||
@@ -519,7 +520,7 @@ const PURGE_LOCK_KEY = 8_723_103
|
||||
|
||||
/** Удаляет данные одним transaction; ключи и пиры не меняются. */
|
||||
export async function purgeTrafficFlowStore(): Promise<FlowPurgeDto> {
|
||||
return withAdvisoryLock(PURGE_LOCK_KEY, () => executeTrafficFlowPurge({
|
||||
return withTrafficFlowPurgeLock(() => pool.connect(), PURGE_LOCK_KEY, () => executeTrafficFlowPurge({
|
||||
stop: async () => {
|
||||
wantListen = false
|
||||
await stopWorkerProcessAsync()
|
||||
|
||||
Reference in new issue
Block a user