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)
121 lines
3.2 KiB
JavaScript
121 lines
3.2 KiB
JavaScript
/**
|
|
* Lightweight job tray for on-demand agent operations.
|
|
*/
|
|
import { EventEmitter } from 'events'
|
|
import { randomUUID } from 'crypto'
|
|
import { Pushes } from '../../shared/protocol.js'
|
|
import { peers } from '../core/peer-registry.js'
|
|
import { getCollector } from './collector.js'
|
|
import { getStore } from './store.js'
|
|
import { getAnomalyEngine } from './anomaly.js'
|
|
import { jobExportSnapshot, jobPrometheusPush } from './export.js'
|
|
|
|
const JOB_HANDLERS = {
|
|
collectOnce: async () => {
|
|
const c = getCollector()
|
|
c._tick()
|
|
return { ok: true, sampleCount: c.sampleCount }
|
|
},
|
|
snapshot: async () => {
|
|
return { ok: true, latest: getStore().latestValues() }
|
|
},
|
|
exportSnapshot: jobExportSnapshot,
|
|
prometheusPush: jobPrometheusPush,
|
|
retrainAnomaly: async (args = {}) => {
|
|
const eng = getAnomalyEngine()
|
|
return eng.retrain(getStore(), {
|
|
warnZ: args.warnZ != null ? Number(args.warnZ) : undefined,
|
|
critZ: args.critZ != null ? Number(args.critZ) : undefined,
|
|
minPoints: args.minPoints != null ? Number(args.minPoints) : undefined,
|
|
})
|
|
},
|
|
gcBuffers: async (args = {}) => {
|
|
const { pruneHistory } = await import('./retention.js')
|
|
const result = await pruneHistory({
|
|
dryRun: Boolean(args.dryRun),
|
|
beforeMs: args.beforeMs != null ? Number(args.beforeMs) : undefined,
|
|
force: true,
|
|
})
|
|
return { ok: result.success !== false, ...result }
|
|
},
|
|
}
|
|
|
|
export class JobService extends EventEmitter {
|
|
constructor() {
|
|
super()
|
|
/** @type {Map<string, import('../../shared/data-model.js').JobRecord>} */
|
|
this.jobs = new Map()
|
|
}
|
|
|
|
list() {
|
|
return [...this.jobs.values()].sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0))
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @param {object} [args]
|
|
*/
|
|
async run(name, args = {}) {
|
|
const handler = JOB_HANDLERS[name]
|
|
if (!handler) {
|
|
return { success: false, error: `unknown job: ${name}`, known: Object.keys(JOB_HANDLERS) }
|
|
}
|
|
const id = randomUUID()
|
|
/** @type {import('../../shared/data-model.js').JobRecord} */
|
|
const rec = {
|
|
id,
|
|
name,
|
|
status: 'running',
|
|
startedAt: Date.now(),
|
|
finishedAt: null,
|
|
}
|
|
this.jobs.set(id, rec)
|
|
this._push(rec)
|
|
try {
|
|
rec.result = await handler(args)
|
|
rec.status = 'done'
|
|
rec.finishedAt = Date.now()
|
|
} catch (err) {
|
|
rec.status = 'failed'
|
|
rec.error = err.message
|
|
rec.finishedAt = Date.now()
|
|
}
|
|
this._push(rec)
|
|
return { success: rec.status === 'done', job: rec }
|
|
}
|
|
|
|
cancel(id) {
|
|
const rec = this.jobs.get(id)
|
|
if (!rec) return { success: false, error: 'unknown job' }
|
|
if (rec.status === 'running') {
|
|
rec.status = 'cancelled'
|
|
rec.finishedAt = Date.now()
|
|
this._push(rec)
|
|
}
|
|
return { success: true, job: rec }
|
|
}
|
|
|
|
_push(rec) {
|
|
for (const session of peers.list()) {
|
|
if (session.closed) continue
|
|
try {
|
|
session.push(Pushes.job, rec)
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @type {JobService|null} */
|
|
let singleton = null
|
|
|
|
export function getJobs() {
|
|
if (!singleton) singleton = new JobService()
|
|
return singleton
|
|
}
|
|
|
|
export function knownJobNames() {
|
|
return Object.keys(JOB_HANDLERS)
|
|
}
|