183 lines
6.3 KiB
JavaScript
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))
|
|
}
|