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 <[email protected]>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-11-06 22:36:53 +07:00
1 parent 0b9b1cad54
commit 80af888cdb
9 files changed
+199 -17

No files matched your search

@@ -0,0 +1,18 @@
import { describe, expect, it } from 'vitest'
import { mapConcurrent } from './sync.js'
describe('mapConcurrent', () => {
it('keeps order and caps parallelism', async () => {
let active = 0
let peak = 0
const out = await mapConcurrent([5, 1, 4, 2, 3, 0], 2, async (n) => {
active++
peak = Math.max(peak, active)
await new Promise((r) => setTimeout(r, n))
active--
return n * 10
})
expect(out).toEqual([50, 10, 40, 20, 30, 0])
expect(peak).toBe(2)
})
})
+23 -14
View File
@@ -88,6 +88,26 @@ async function mapVdsWithSpecs(
return vps
}
/** vds.edit lookups are one HTTP call per VPS — run a few at once, not all (hoster rate limits). */
const VDS_SPECS_CONCURRENCY = 4
export async function mapConcurrent<T, R>(
items: T[],
limit: number,
fn: (item: T) => Promise<R>,
): Promise<R[]> {
const out: R[] = new Array(items.length)
let next = 0
const worker = async () => {
while (next < items.length) {
const i = next++
out[i] = await fn(items[i]!)
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker))
return out
}
export async function syncFromBillmanager(
account: BillmanagerSyncAccount,
opts: SyncFromBillmanagerOptions = {},
@@ -138,20 +158,9 @@ export async function syncFromBillmanager(
const syncSummary: SyncSummary = { added: [], updated: [], paymentsAdded: 0 }
if (fetchVpsPayments) {
const mappedRaw: Awaited<ReturnType<typeof mapVdsWithSpecs>>[] = []
for (const item of vdsItems) {
mappedRaw.push(
await mapVdsWithSpecs(
profile,
item,
providerId,
accountId,
tariffItems,
apiBaseUrl,
authinfo,
),
)
}
const mappedRaw = await mapConcurrent(vdsItems, VDS_SPECS_CONCURRENCY, (item) =>
mapVdsWithSpecs(profile, item, providerId, accountId, tariffItems, apiBaseUrl, authinfo),
)
const mappedList = profile.map.enrichVdsBatch
? profile.map.enrichVdsBatch(mappedRaw, {
balance: dashboardInfo?.balance ?? null,
+2 -2
View File
@@ -1,4 +1,4 @@
import { getSnapshot } from '@cfdm/db/repositories/snapshot'
import { getDashboardSnapshot } from '@cfdm/db/repositories/snapshot'
import { countExpiringWithin7Days, countInventoryIssues } from '@cfdm/shared/utils/inventory-health'
import { accountBalanceApi } from '@cfdm/shared/utils/account-balance'
import { isSyncApiType } from '@cfdm/shared/contracts/provider'
@@ -76,7 +76,7 @@ export interface DashboardStats {
}
export function computeDashboardStats(): DashboardStats {
const snap = getSnapshot()
const snap = getDashboardSnapshot()
const now = new Date()
const activeVps = snap.vps.filter((v) => v.status === 'active')
+15 -1
View File
@@ -1,5 +1,5 @@
import { eq, sql } from 'drizzle-orm'
import { getDb, schema, runWithSpaceAsync, MAIN_SPACE_ID } from '@cfdm/db'
import { getDb, getSqlite, pruneOldLogs, schema, runWithSpaceAsync, MAIN_SPACE_ID } from '@cfdm/db'
import { settingsRepository } from '@cfdm/db/repositories/settings'
import { resolveSyncAccount, getProviderAdapter, type SyncReadyAccount } from './providers/index.js'
@@ -25,6 +25,8 @@ let notifyIntervalId: ReturnType<typeof setInterval> | null = null
let uptimeIntervalId: ReturnType<typeof setInterval> | null = null
let cfdmSyncIntervalId: ReturnType<typeof setInterval> | null = null
let cfdmSyncInFlight = false
let retentionIntervalId: ReturnType<typeof setInterval> | null = null
const RETENTION_INTERVAL_MS = 24 * 60 * 60 * 1000
type AccountRow = typeof schema.providerAccounts.$inferSelect
type SettingsRow = typeof schema.settings.$inferSelect
@@ -109,6 +111,12 @@ export const runNotificationTick = () => exclusive('notify', notificationTick)
export const runScheduledSync = () => exclusive('sync', scheduledSync)
export const runScheduledSyncTariffs = () => exclusive('tariffs', scheduledSyncTariffs)
export const runScheduledUptimeChecks = () => exclusive('uptime', scheduledUptimeChecks)
export const runRetention = () =>
exclusive('retention', async () => {
const r = pruneOldLogs(getSqlite())
const total = r.notificationLog + r.syncLog + r.vpsHealthChecks + r.staleSyncs
if (total > 0) console.log('Retention:', JSON.stringify(r))
})
async function notificationTick(): Promise<void> {
for (const settings of allSettings()) {
@@ -334,6 +342,8 @@ export function startScheduler(): void {
uptimeIntervalId = null
if (cfdmSyncIntervalId) clearInterval(cfdmSyncIntervalId)
cfdmSyncIntervalId = null
if (retentionIntervalId) clearInterval(retentionIntervalId)
retentionIntervalId = null
try {
const rows = allSettings()
@@ -350,6 +360,8 @@ export function startScheduler(): void {
)
void runNotificationTick()
void runScheduledUptimeChecks()
retentionIntervalId = setInterval(() => void runRetention(), RETENTION_INTERVAL_MS)
void runRetention()
const parts = [
`notify every ${notifyInterval} min`,
@@ -393,6 +405,8 @@ export function stopScheduler(): void {
uptimeIntervalId = null
if (cfdmSyncIntervalId) clearInterval(cfdmSyncIntervalId)
cfdmSyncIntervalId = null
if (retentionIntervalId) clearInterval(retentionIntervalId)
retentionIntervalId = null
}
export function restartScheduler(): void {