Usage — total data dir, Corestore, memory rings, warm HyperDB sample
CI / test (push) Successful in 1m2s
Release rolling / release (push) Has been cancelled

Hot memory — 15m → 24h (or custom points)
Warm memory — downsampled ring + samples-per-bucket
Disk history — auto-rotate by age (1 day → 1 year, forever, or custom days)
Soft disk budget + max warm points
Auto prune on a configurable hour interval
Dry-run prune / Prune now / Save to agent (admin role)
This commit is contained in:
Raven Scott
2026-07-19 12:55:13 -04:00
parent b5b2907496
commit c3b4240e47
26 changed files with 1610 additions and 17 deletions
+8 -1
View File
@@ -1,5 +1,5 @@
import test from 'brittle'
import { formatMib, formatKilobitsPerSec } from '../shared/format.js'
import { formatMib, formatKilobitsPerSec, formatBytes } from '../shared/format.js'
test('formatMib scales through binary units', async (t) => {
t.is(formatMib(null), '—')
@@ -21,3 +21,10 @@ test('formatKilobitsPerSec scales through SI bit rates', async (t) => {
t.is(formatKilobitsPerSec(2_500_000), '2.5 Gb/s')
t.is(formatKilobitsPerSec(3_000_000_000), '3 Tb/s')
})
test('formatBytes scales binary units', async (t) => {
t.is(formatBytes(null), '—')
t.is(formatBytes(0), '0 B')
t.is(formatBytes(512), '512 B')
t.is(formatBytes(2048), '2 KiB')
})
+4
View File
@@ -41,6 +41,10 @@ test('method roles cover monitoring surface', (t) => {
'listChildPeers',
'getWeights',
'queryLogs',
'getStorageInfo',
'getRetentionConfig',
'setRetentionConfig',
'pruneHistory',
]) {
t.ok(MethodRoles[m], m)
t.is(Methods[m], m)
+144
View File
@@ -0,0 +1,144 @@
import test from 'brittle'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import {
defaultRetentionConfig,
normalizeRetentionConfig,
retentionCutoffMs,
RETENTION_PRESETS,
matchPointsPreset,
HOT_PRESETS,
} from '../shared/retention.js'
import { formatBytes } from '../shared/format.js'
import { MetricStore } from '../server/services/store.js'
import { PearDataModel } from '../server/db/model.js'
import Corestore from 'corestore'
import { validateMethodArgs } from '../shared/schema.js'
import { MethodRoles, Methods, Roles } from '../shared/protocol.js'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const TMP = path.join(__dirname, '..', 'tmp-retention-test')
test('normalizeRetentionConfig applies presets', (t) => {
const cfg = normalizeRetentionConfig({
warmRetentionPreset: '1m',
tier0Points: 900,
})
t.is(cfg.warmRetentionMs, RETENTION_PRESETS['1m'].ms)
t.is(cfg.tier0Points, 900)
t.is(matchPointsPreset(900, HOT_PRESETS), '15m')
})
test('retentionCutoffMs respects forever / disabled', (t) => {
const now = 1_700_000_000_000
t.is(retentionCutoffMs({ warmRetentionEnabled: false, warmRetentionMs: 1000 }, now), null)
t.is(retentionCutoffMs({ warmRetentionEnabled: true, warmRetentionMs: 0 }, now), null)
t.is(
retentionCutoffMs({ warmRetentionEnabled: true, warmRetentionMs: 1000 }, now),
now - 1000
)
})
test('formatBytes scales', (t) => {
t.is(formatBytes(0), '0 B')
t.is(formatBytes(1024), '1 KiB')
t.is(formatBytes(1536 * 1024 * 1024), '1.5 GiB')
})
test('MetricStore setRetention trims rings', (t) => {
const store = new MetricStore()
const ts = Date.now()
for (let i = 0; i < 100; i++) {
store.ingest([
{
chart: 'system.cpu',
context: 'system.cpu',
ts: ts + i * 1000,
values: { user: 1, idle: 99 },
},
])
}
t.ok(store.series.get('system.cpu').points.length >= 100)
store.setRetention({ tier0Max: 10, tier1Max: 5, tier1Every: 60 })
t.is(store.tier0Max, 10)
t.is(store.series.get('system.cpu').points.length, 10)
const mem = store.memoryStats()
t.is(mem.tier0Max, 10)
t.ok(mem.tier0Points <= 10)
})
test('protocol exposes data manager methods', (t) => {
for (const m of ['getStorageInfo', 'getRetentionConfig', 'setRetentionConfig', 'pruneHistory']) {
t.ok(MethodRoles[m], m)
t.is(Methods[m], m)
}
t.is(MethodRoles.setRetentionConfig, Roles.admin)
t.is(MethodRoles.pruneHistory, Roles.admin)
t.ok(validateMethodArgs('getStorageInfo', {}).ok)
t.ok(validateMethodArgs('setRetentionConfig', { tier0Points: 900 }).ok)
t.ok(validateMethodArgs('pruneHistory', { dryRun: true }).ok)
})
test('hyperdb deleteMetricPointsBefore removes old rows', async (t) => {
fs.rmSync(TMP, { recursive: true, force: true })
const store = new Corestore(path.join(TMP, 'corestore'))
await store.ready()
const core = store.get({ name: 'peardata-meta' })
const model = new PearDataModel(core, { autoUpdate: true })
await model.ready()
try {
const now = Date.now()
await model.putMetricPoints([
{
chart: 'system.cpu',
context: 'system.cpu',
ts: now - 10_000,
values: { user: 1 },
tier: 1,
},
{
chart: 'system.cpu',
context: 'system.cpu',
ts: now - 1000,
values: { user: 2 },
tier: 1,
},
{
chart: 'system.cpu',
context: 'system.cpu',
ts: now,
values: { user: 3 },
tier: 1,
},
])
const dry = await model.deleteMetricPointsBefore(now - 2000, {
charts: ['system.cpu'],
dryRun: true,
})
t.is(dry.deleted, 1)
const del = await model.deleteMetricPointsBefore(now - 2000, {
charts: ['system.cpu'],
dryRun: false,
})
t.is(del.deleted, 1)
const rows = await model.queryMetricPoints({
chart: 'system.cpu',
afterMs: now - 20_000,
beforeMs: now + 1000,
})
t.is(rows.length, 2)
t.is(rows[0].values.user, 2)
} finally {
await model.close().catch(() => {})
await store.close().catch(() => {})
fs.rmSync(TMP, { recursive: true, force: true })
}
})
test('defaultRetentionConfig has sane defaults', (t) => {
const d = defaultRetentionConfig()
t.is(d.warmRetentionPreset, '1w')
t.ok(d.autoPruneEnabled)
t.ok(d.tier0Points > 0)
})