perf(api): чистить старые журналы и облегчить синк и dashboard
- раз в сутки удаляются notification_log и sync_log старше 90 дней и vps_health_checks старше 30 дней; синки, зависшие в running дольше часа, помечаются ошибкой - dashboard stats не грузит тарифы, домены и проекты - запросы характеристик VPS в BILLmanager идут по 4 параллельно Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0b9b1cad54
commit
80af888cdb
9 files changed
+199
-17
No files matched your search
@@ -98,3 +98,4 @@ export {
|
||||
consolidateProviderApiFromAccounts,
|
||||
heuristicBillmanagerProviderApi,
|
||||
} from './maintenance/consolidate-api.js'
|
||||
export { pruneOldLogs, RETENTION_DAYS, type PruneResult } from './maintenance/retention.js'
|
||||
@@ -0,0 +1,29 @@
|
||||
import Database from 'better-sqlite3'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { ensureRuntimeSchema, resetRuntimeMigrate } from '../runtime-migrate.js'
|
||||
import { pruneOldLogs } from './retention.js'
|
||||
|
||||
describe('pruneOldLogs', () => {
|
||||
it('drops old rows, keeps fresh ones, closes stale running syncs', () => {
|
||||
const db = new Database(':memory:')
|
||||
resetRuntimeMigrate()
|
||||
ensureRuntimeSchema(db)
|
||||
resetRuntimeMigrate()
|
||||
const now = new Date('2026-06-01T00:00:00Z')
|
||||
const old = '2026-01-01T00:00:00Z'
|
||||
const fresh = '2026-05-31T23:30:00Z'
|
||||
db.exec(`PRAGMA foreign_keys = OFF`)
|
||||
for (const [id, at] of [['n-old', old], ['n-new', fresh]]) {
|
||||
db.prepare(`INSERT INTO notification_log (id, event, channel, status, createdAt) VALUES (?, 'e', 'telegram', 'sent', ?)`).run(id, at)
|
||||
db.prepare(`INSERT INTO vps_health_checks (id, vpsId, checkedAt, status) VALUES (?, 'v', ?, 'up')`).run(id, at)
|
||||
}
|
||||
db.prepare(`INSERT INTO sync_log (id, accountId, startedAt, status) VALUES ('s-old', 'a', ?, 'ok')`).run(old)
|
||||
db.prepare(`INSERT INTO sync_log (id, accountId, startedAt, status) VALUES ('s-stuck', 'a', '2026-05-31T20:00:00Z', 'running')`).run()
|
||||
db.prepare(`INSERT INTO sync_log (id, accountId, startedAt, status) VALUES ('s-live', 'a', ?, 'running')`).run(fresh)
|
||||
|
||||
expect(pruneOldLogs(db, now)).toEqual({ notificationLog: 1, syncLog: 1, vpsHealthChecks: 1, staleSyncs: 1 })
|
||||
const status = (id: string) => (db.prepare(`SELECT status FROM sync_log WHERE id = ?`).get(id) as { status: string }).status
|
||||
expect(status('s-stuck')).toBe('error')
|
||||
expect(status('s-live')).toBe('running')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import type Database from 'better-sqlite3'
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000
|
||||
|
||||
export const RETENTION_DAYS = {
|
||||
notificationLog: 90,
|
||||
syncLog: 90,
|
||||
vpsHealthChecks: 30,
|
||||
} as const
|
||||
|
||||
/** A sync that is still "running" after this long was cut off by a crash/restart. */
|
||||
const STALE_RUNNING_SYNC_MS = 60 * 60 * 1000
|
||||
|
||||
export interface PruneResult {
|
||||
notificationLog: number
|
||||
syncLog: number
|
||||
vpsHealthChecks: number
|
||||
staleSyncs: number
|
||||
}
|
||||
|
||||
/** Delete old journal rows (all spaces) and close sync_log rows left 'running' by a dead process. */
|
||||
export function pruneOldLogs(sqlite: Database.Database, now = new Date()): PruneResult {
|
||||
const before = (days: number) => new Date(now.getTime() - days * DAY_MS).toISOString()
|
||||
const nowIso = now.toISOString()
|
||||
return sqlite.transaction(() => ({
|
||||
staleSyncs: sqlite
|
||||
.prepare(
|
||||
`UPDATE sync_log SET status = 'error', finishedAt = ?, error = 'Прервано: процесс перезапущен'
|
||||
WHERE status = 'running' AND startedAt < ?`,
|
||||
)
|
||||
.run(nowIso, new Date(now.getTime() - STALE_RUNNING_SYNC_MS).toISOString()).changes,
|
||||
notificationLog: sqlite
|
||||
.prepare(`DELETE FROM notification_log WHERE createdAt < ?`)
|
||||
.run(before(RETENTION_DAYS.notificationLog)).changes,
|
||||
syncLog: sqlite
|
||||
.prepare(`DELETE FROM sync_log WHERE startedAt < ?`)
|
||||
.run(before(RETENTION_DAYS.syncLog)).changes,
|
||||
vpsHealthChecks: sqlite
|
||||
.prepare(`DELETE FROM vps_health_checks WHERE checkedAt < ?`)
|
||||
.run(before(RETENTION_DAYS.vpsHealthChecks)).changes,
|
||||
}))()
|
||||
}
|
||||
@@ -49,6 +49,23 @@ function listVpsWithShared(): VpsDto[] {
|
||||
return [...owned, ...shared]
|
||||
}
|
||||
|
||||
export type DashboardSnapshot = Pick<
|
||||
Snapshot,
|
||||
'vps' | 'providers' | 'providerAccounts' | 'payments' | 'balanceLedger' | 'syncLog'
|
||||
>
|
||||
|
||||
/** What dashboard stats read — skips tariffs, domains, projects and settings of the full snapshot. */
|
||||
export function getDashboardSnapshot(): DashboardSnapshot {
|
||||
return {
|
||||
vps: listVpsWithShared(),
|
||||
providers: providersRepository.list(),
|
||||
providerAccounts: providerAccountsRepository.list(),
|
||||
payments: paymentsRepository.list(),
|
||||
balanceLedger: balanceLedgerRepository.list(),
|
||||
syncLog: syncLogRepository.listRecent(50),
|
||||
}
|
||||
}
|
||||
|
||||
export function getSnapshot(): Snapshot {
|
||||
const spaceId = getCurrentSpaceId()
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user