fix(traffic): объединять чтения истории общего сервера

This commit is contained in:
Denozordec committed 2026-10-13 14:15:44 +07:00
1 parent 7b88416415
commit a68bd61e58
18 files changed
+1373 -30

No files matched your search

@@ -0,0 +1,33 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import vm from 'node:vm'
import ts from 'typescript'
import {buildTrafficFromSamples,mergeBuiltTraffic} from './traffic-rate.js'
import {applyTrafficFreshness,trafficBindingStatus} from './traffic-freshness.js'
const source=fs.readFileSync(new URL('./traffic-users.ts',import.meta.url),'utf8')
const ast=ts.createSourceFile('traffic-users.ts',source,ts.ScriptTarget.Latest,true)
const names=['seriesFromBuilt','buildUserTrafficList']
const functions=ast.statements.filter(n=>ts.isFunctionDeclaration(n)&&n.name&&names.includes(n.name.text)).map(n=>n.getText(ast)).join('\n')
assert.equal(ast.statements.filter(n=>ts.isFunctionDeclaration(n)&&n.name&&names.includes(n.name.text)).length,2)
let calls=0,fail=false
const at=Date.parse('2026-10-13T00:00:00Z')
const users=Array.from({length:100},(_,i)=>({id:String(i),login:'user'+i,name:'User '+i,role:'user',active:true,
bindings:[{serverId:1,interfaceName:'ether1',peerPublicKey:'',userId:String(i),id:'binding'+i}]}))
const context=vm.createContext({Date,Map,Promise,buildTrafficFromSamples,mergeBuiltTraffic,applyTrafficFreshness,trafficBindingStatus,
listUsers:async()=>users,servers:{},db:{select:()=>({from:async()=>[{id:1,enabled:true,updatedAt:new Date(at).toISOString()}]})},
readTrafficCollectionContext:async()=>({settings:{enabled:true,intervalSec:30},states:new Map()}),
readServerSamplesInRange:async()=>{calls++;await new Promise(resolve=>setTimeout(resolve,5));if(fail)throw Error('fixture read failed');
return [0,30_000].map((offset,i)=>({interfaceName:'ether1',sampledAt:new Date(at+offset).toISOString(),rxBytes:i*1024,txBytes:0,rxBps:0,txBps:0,running:true,disabled:false}))},
})
vm.runInContext(ts.transpileModule(functions,{compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.CommonJS}}).outputText.replace(/^.*exports.*$/gm,''),context)
const build=context.buildUserTrafficList as (start:number,end:number)=>Promise<Array<{rxTotal:number;interfaces:unknown[]}>>
const rows=await build(at,at+30_000)
assert.equal(calls,1,'100 concurrent users share one server history read')
assert.equal(rows.length,100);assert(rows.every(r=>r.rxTotal===1024/1024**3&&r.interfaces.length===1))
await build(at,at+60_000);assert.equal(calls,2,'cache is scoped to a request and never reuses an older period')
fail=true;await assert.rejects(build(at,at+60_000),/fixture read failed/);assert.equal(calls,3)
fail=false;await build(at,at+60_000);assert.equal(calls,4,'a failed request cannot poison later reads')
users[99].bindings[0].serverId=2
await build(at,at+60_000);assert.equal(calls,6,'different server IDs never share a cached history')
console.log('user traffic: 100 concurrent consumers/one server read/request scope/failure/recovery passed')
+6 -5
View File
@@ -80,18 +80,19 @@ export async function buildUserTrafficList(rangeStartMs: number, rangeEndMs: num
const users = await listUsers()
const [context, allServers] = await Promise.all([readTrafficCollectionContext(), db.select().from(servers)])
const catalog = new Map(allServers.map(s => [s.id, s]))
const sampleCache = new Map<number, Awaited<ReturnType<typeof readServerSamplesInRange>>>()
const sampleCache = new Map<number, ReturnType<typeof readServerSamplesInRange>>()
return Promise.all(users.map(async (user) => {
const parts: BuiltTrafficSeries[] = []
const interfaces: BoundIfaceTrafficDto[] = []
for (const b of user.bindings) {
let rows = sampleCache.get(b.serverId)
if (!rows) {
rows = await readServerSamplesInRange(b.serverId, sinceIso)
sampleCache.set(b.serverId, rows)
let pending = sampleCache.get(b.serverId)
if (!pending) {
pending = readServerSamplesInRange(b.serverId, sinceIso)
sampleCache.set(b.serverId, pending)
}
const rows = await pending
const peerKey = b.peerPublicKey ?? ""
const built = buildTrafficFromSamples(rows, rangeStartMs, rangeEndMs, b.interfaceName, peerKey, true)
const server = catalog.get(b.serverId)