feat(cron): add scheduler status endpoint

This commit is contained in:
Peter Steinberger
2025-12-13 03:43:40 +00:00
parent a641250da6
commit 415cb857d9
6 changed files with 1075 additions and 0 deletions

View File

@@ -59,6 +59,7 @@ export class CronService {
private timer: NodeJS.Timeout | null = null;
private running = false;
private op: Promise<unknown> = Promise.resolve();
private warnedDisabled = false;
constructor(deps: CronServiceDeps) {
this.deps = {
@@ -94,6 +95,19 @@ export class CronService {
this.timer = null;
}
async status() {
return await this.locked(async () => {
await this.ensureLoaded();
return {
enabled: this.deps.cronEnabled,
storePath: this.deps.storePath,
jobs: this.store?.jobs.length ?? 0,
nextWakeAtMs:
this.deps.cronEnabled === true ? (this.nextWakeAtMs() ?? null) : null,
};
});
}
async list(opts?: { includeDisabled?: boolean }) {
return await this.locked(async () => {
await this.ensureLoaded();
@@ -109,6 +123,7 @@ export class CronService {
async add(input: CronJobCreate) {
return await this.locked(async () => {
this.warnIfDisabled("add");
await this.ensureLoaded();
const now = this.deps.nowMs();
const id = crypto.randomUUID();
@@ -142,6 +157,7 @@ export class CronService {
async update(id: string, patch: CronJobPatch) {
return await this.locked(async () => {
this.warnIfDisabled("update");
await this.ensureLoaded();
const job = this.findJobOrThrow(id);
const now = this.deps.nowMs();
@@ -176,6 +192,7 @@ export class CronService {
async remove(id: string) {
return await this.locked(async () => {
this.warnIfDisabled("remove");
await this.ensureLoaded();
const before = this.store?.jobs.length ?? 0;
if (!this.store) return { ok: false, removed: false };
@@ -190,6 +207,7 @@ export class CronService {
async run(id: string, mode?: "due" | "force") {
return await this.locked(async () => {
this.warnIfDisabled("run");
await this.ensureLoaded();
const job = this.findJobOrThrow(id);
const now = this.deps.nowMs();
@@ -232,6 +250,16 @@ export class CronService {
this.store = { version: 1, jobs: loaded.jobs ?? [] };
}
private warnIfDisabled(action: string) {
if (this.deps.cronEnabled) return;
if (this.warnedDisabled) return;
this.warnedDisabled = true;
this.deps.log.warn(
{ enabled: false, action, storePath: this.deps.storePath },
"cron: scheduler disabled; jobs will not run automatically",
);
}
private async persist() {
if (!this.store) return;
await saveCronStore(this.deps.storePath, this.store);

View File

@@ -22,6 +22,8 @@ import {
CronRunParamsSchema,
type CronRunsParams,
CronRunsParamsSchema,
type CronStatusParams,
CronStatusParamsSchema,
type CronUpdateParams,
CronUpdateParamsSchema,
ErrorCodes,
@@ -74,6 +76,9 @@ export const validateAgentParams = ajv.compile(AgentParamsSchema);
export const validateWakeParams = ajv.compile<WakeParams>(WakeParamsSchema);
export const validateCronListParams =
ajv.compile<CronListParams>(CronListParamsSchema);
export const validateCronStatusParams = ajv.compile<CronStatusParams>(
CronStatusParamsSchema,
);
export const validateCronAddParams =
ajv.compile<CronAddParams>(CronAddParamsSchema);
export const validateCronUpdateParams = ajv.compile<CronUpdateParams>(
@@ -115,6 +120,7 @@ export {
WakeParamsSchema,
CronJobSchema,
CronListParamsSchema,
CronStatusParamsSchema,
CronAddParamsSchema,
CronUpdateParamsSchema,
CronRemoveParamsSchema,
@@ -148,6 +154,7 @@ export type {
WakeParams,
CronJob,
CronListParams,
CronStatusParams,
CronAddParams,
CronUpdateParams,
CronRemoveParams,

View File

@@ -316,6 +316,11 @@ export const CronListParamsSchema = Type.Object(
{ additionalProperties: false },
);
export const CronStatusParamsSchema = Type.Object(
{},
{ additionalProperties: false },
);
export const CronAddParamsSchema = Type.Object(
{
name: Type.Optional(Type.String()),
@@ -438,6 +443,7 @@ export const ProtocolSchemas: Record<string, TSchema> = {
WakeParams: WakeParamsSchema,
CronJob: CronJobSchema,
CronListParams: CronListParamsSchema,
CronStatusParams: CronStatusParamsSchema,
CronAddParams: CronAddParamsSchema,
CronUpdateParams: CronUpdateParamsSchema,
CronRemoveParams: CronRemoveParamsSchema,
@@ -467,6 +473,7 @@ export type AgentEvent = Static<typeof AgentEventSchema>;
export type WakeParams = Static<typeof WakeParamsSchema>;
export type CronJob = Static<typeof CronJobSchema>;
export type CronListParams = Static<typeof CronListParamsSchema>;
export type CronStatusParams = Static<typeof CronStatusParamsSchema>;
export type CronAddParams = Static<typeof CronAddParamsSchema>;
export type CronUpdateParams = Static<typeof CronUpdateParamsSchema>;
export type CronRemoveParams = Static<typeof CronRemoveParamsSchema>;

View File

@@ -76,6 +76,7 @@ import {
validateCronRemoveParams,
validateCronRunParams,
validateCronRunsParams,
validateCronStatusParams,
validateCronUpdateParams,
validateRequestFrame,
validateSendParams,
@@ -96,6 +97,7 @@ const METHODS = [
"set-heartbeats",
"wake",
"cron.list",
"cron.status",
"cron.add",
"cron.update",
"cron.remove",
@@ -1116,6 +1118,23 @@ export async function startGatewayServer(
respond(true, { jobs }, undefined);
break;
}
case "cron.status": {
const params = (req.params ?? {}) as Record<string, unknown>;
if (!validateCronStatusParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid cron.status params: ${formatValidationErrors(validateCronStatusParams.errors)}`,
),
);
break;
}
const status = await cron.status();
respond(true, status, undefined);
break;
}
case "cron.add": {
const params = (req.params ?? {}) as Record<string, unknown>;
if (!validateCronAddParams(params)) {