Files
peardata/server/services/jobs.js
T
Raven Scott c3b4240e47
CI / test (push) Successful in 1m2s
Release rolling / release (push) Has been cancelled
Usage — total data dir, Corestore, memory rings, warm HyperDB sample
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)
2026-07-19 12:55:13 -04:00

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)
}