Files
peardata/shared/retention.js
T
Raven Scott c541c27ad8
CI / test (push) Successful in 1m17s
Release rolling / release (push) Successful in 8m15s
Prune work
2026-07-30 12:38:17 -04:00

183 lines
6.3 KiB
JavaScript

/**
* Retention presets + config normalization (client + agent).
*/
/** @typedef {{
* tier0Points: number,
* tier1Points: number,
* tier1Every: number,
* warmRetentionEnabled: boolean,
* warmRetentionPreset: string,
* warmRetentionMs: number,
* warmMaxBytes: number,
* warmMaxPoints: number,
* autoPruneEnabled: boolean,
* autoPruneIntervalMs: number,
* lastPruneAt: number|null,
* lastPruneDeleted: number,
* lastPruneBytesFreed: number,
* }} RetentionConfig */
export const RETENTION_PRESETS = Object.freeze({
'1d': { id: '1d', label: '1 day', ms: 1 * 24 * 60 * 60 * 1000 },
'1w': { id: '1w', label: '1 week', ms: 7 * 24 * 60 * 60 * 1000 },
'2w': { id: '2w', label: '2 weeks', ms: 14 * 24 * 60 * 60 * 1000 },
'1m': { id: '1m', label: '1 month', ms: 30 * 24 * 60 * 60 * 1000 },
'3m': { id: '3m', label: '3 months', ms: 90 * 24 * 60 * 60 * 1000 },
'6m': { id: '6m', label: '6 months', ms: 180 * 24 * 60 * 60 * 1000 },
'1y': { id: '1y', label: '1 year', ms: 365 * 24 * 60 * 60 * 1000 },
forever: { id: 'forever', label: 'Forever', ms: 0 },
custom: { id: 'custom', label: 'Custom', ms: null },
})
/** Hot (1s) ring size presets → points. */
export const HOT_PRESETS = Object.freeze({
'15m': { id: '15m', label: '15 minutes', points: 900 },
'1h': { id: '1h', label: '1 hour', points: 3600 },
'6h': { id: '6h', label: '6 hours', points: 21600 },
'12h': { id: '12h', label: '12 hours', points: 43200 },
'24h': { id: '24h', label: '24 hours', points: 86400 },
custom: { id: 'custom', label: 'Custom', points: null },
})
/** In-memory warm ring presets (@ ~1m resolution). */
export const WARM_MEM_PRESETS = Object.freeze({
'1h': { id: '1h', label: '1 hour', points: 60 },
'6h': { id: '6h', label: '6 hours', points: 360 },
'24h': { id: '24h', label: '24 hours', points: 1440 },
'7d': { id: '7d', label: '7 days', points: 10080 },
custom: { id: 'custom', label: 'Custom', points: null },
})
/** Soft disk budget presets (bytes). 0 = unlimited. */
export const DISK_BUDGET_PRESETS = Object.freeze({
unlimited: { id: 'unlimited', label: 'Unlimited', bytes: 0 },
'256mb': { id: '256mb', label: '256 MB', bytes: 256 * 1024 * 1024 },
'512mb': { id: '512mb', label: '512 MB', bytes: 512 * 1024 * 1024 },
'1gb': { id: '1gb', label: '1 GB', bytes: 1024 * 1024 * 1024 },
'5gb': { id: '5gb', label: '5 GB', bytes: 5 * 1024 * 1024 * 1024 },
'20gb': { id: '20gb', label: '20 GB', bytes: 20 * 1024 * 1024 * 1024 },
custom: { id: 'custom', label: 'Custom', bytes: null },
})
/**
* @returns {RetentionConfig}
*/
export function defaultRetentionConfig() {
return {
tier0Points: 3600,
tier1Points: 1440,
tier1Every: 60,
warmRetentionEnabled: true,
// 3 months of warm history is a better default than 1y for multi-chart hosts
warmRetentionPreset: '3m',
warmRetentionMs: RETENTION_PRESETS['3m'].ms,
// Soft disk budget (0 = unlimited). Default 1 GiB keeps corestore from ballooning.
warmMaxBytes: DISK_BUDGET_PRESETS['1gb'].bytes,
warmMaxPoints: 0,
autoPruneEnabled: true,
autoPruneIntervalMs: 60 * 60 * 1000,
lastPruneAt: null,
lastPruneDeleted: 0,
lastPruneBytesFreed: 0,
}
}
/**
* @param {Partial<RetentionConfig>|null|undefined} raw
* @returns {RetentionConfig}
*/
export function normalizeRetentionConfig(raw = {}) {
const d = defaultRetentionConfig()
const src = raw && typeof raw === 'object' ? raw : {}
const tier0Points = clampInt(src.tier0Points ?? d.tier0Points, 60, 604_800, d.tier0Points)
const tier1Points = clampInt(src.tier1Points ?? d.tier1Points, 10, 525_600, d.tier1Points)
const tier1Every = clampInt(src.tier1Every ?? d.tier1Every, 5, 3600, d.tier1Every)
let warmRetentionPreset = String(src.warmRetentionPreset || d.warmRetentionPreset)
if (!RETENTION_PRESETS[warmRetentionPreset]) warmRetentionPreset = 'custom'
let warmRetentionMs = Number(src.warmRetentionMs)
if (!Number.isFinite(warmRetentionMs) || warmRetentionMs < 0) warmRetentionMs = d.warmRetentionMs
if (warmRetentionPreset !== 'custom' && warmRetentionPreset !== 'forever') {
warmRetentionMs = RETENTION_PRESETS[warmRetentionPreset].ms
} else if (warmRetentionPreset === 'forever') {
warmRetentionMs = 0
}
const warmRetentionEnabled =
src.warmRetentionEnabled == null ? d.warmRetentionEnabled : Boolean(src.warmRetentionEnabled)
const warmMaxBytes = clampInt(src.warmMaxBytes ?? d.warmMaxBytes, 0, Number.MAX_SAFE_INTEGER, 0)
const warmMaxPoints = clampInt(src.warmMaxPoints ?? d.warmMaxPoints, 0, Number.MAX_SAFE_INTEGER, 0)
const autoPruneEnabled =
src.autoPruneEnabled == null ? d.autoPruneEnabled : Boolean(src.autoPruneEnabled)
const autoPruneIntervalMs = clampInt(
src.autoPruneIntervalMs ?? d.autoPruneIntervalMs,
60_000,
7 * 24 * 60 * 60 * 1000,
d.autoPruneIntervalMs
)
return {
tier0Points,
tier1Points,
tier1Every,
warmRetentionEnabled,
warmRetentionPreset,
warmRetentionMs,
warmMaxBytes,
warmMaxPoints,
autoPruneEnabled,
autoPruneIntervalMs,
lastPruneAt:
src.lastPruneAt != null && Number.isFinite(Number(src.lastPruneAt))
? Number(src.lastPruneAt)
: null,
lastPruneDeleted: clampInt(src.lastPruneDeleted ?? 0, 0, Number.MAX_SAFE_INTEGER, 0),
lastPruneBytesFreed: clampInt(src.lastPruneBytesFreed ?? 0, 0, Number.MAX_SAFE_INTEGER, 0),
}
}
/**
* Resolve cutoff timestamp for age-based prune (null = skip age prune).
* @param {RetentionConfig} cfg
* @param {number} [now]
*/
export function retentionCutoffMs(cfg, now = Date.now()) {
if (!cfg?.warmRetentionEnabled) return null
if (!cfg.warmRetentionMs || cfg.warmRetentionMs <= 0) return null
return now - cfg.warmRetentionMs
}
/**
* @param {number} points
* @param {Record<string, { points: number|null }>} presets
*/
export function matchPointsPreset(points, presets) {
for (const [id, p] of Object.entries(presets)) {
if (id === 'custom') continue
if (p.points === points) return id
}
return 'custom'
}
/**
* @param {number} bytes
*/
export function matchBytesPreset(bytes) {
for (const [id, p] of Object.entries(DISK_BUDGET_PRESETS)) {
if (id === 'custom') continue
if (p.bytes === bytes) return id
}
return 'custom'
}
function clampInt(v, min, max, fallback) {
const n = Math.floor(Number(v))
if (!Number.isFinite(n)) return fallback
return Math.min(max, Math.max(min, n))
}