1 changed files with 239 additions and 0 deletions
@ -0,0 +1,239 @@ |
|||||
|
import { dbGlobal } from "drizzle-pkg/lib/db"; |
||||
|
import { scheduledTasks, taskExecutionLogs } from "drizzle-pkg/lib/schema/scheduler"; |
||||
|
import { eq, desc, and, sql } from "drizzle-orm"; |
||||
|
import log4js from "logger"; |
||||
|
|
||||
|
const logger = log4js.getLogger("SCHEDULER"); |
||||
|
|
||||
|
export interface CreateTaskInput { |
||||
|
name: string; |
||||
|
cronExpression: string; |
||||
|
type: "function" | "http"; |
||||
|
functionName?: string; |
||||
|
functionPayload?: string; |
||||
|
httpMethod?: string; |
||||
|
httpUrl?: string; |
||||
|
httpHeaders?: string; |
||||
|
httpBody?: string; |
||||
|
catchUp?: number; |
||||
|
enabled?: number; |
||||
|
maxRetries?: number; |
||||
|
retryDelaySeconds?: number; |
||||
|
timeoutSeconds?: number; |
||||
|
} |
||||
|
|
||||
|
export interface UpdateTaskInput extends Partial<CreateTaskInput> {} |
||||
|
|
||||
|
function uuid() { |
||||
|
return crypto.randomUUID(); |
||||
|
} |
||||
|
|
||||
|
function now() { |
||||
|
return new Date(); |
||||
|
} |
||||
|
|
||||
|
// ---- Task CRUD ----
|
||||
|
|
||||
|
export async function listTasks(opts: { |
||||
|
page?: number; |
||||
|
pageSize?: number; |
||||
|
type?: string; |
||||
|
enabled?: number; |
||||
|
}) { |
||||
|
const page = opts.page ?? 1; |
||||
|
const pageSize = opts.pageSize ?? 20; |
||||
|
const conditions = []; |
||||
|
|
||||
|
if (opts.type) conditions.push(eq(scheduledTasks.type, opts.type)); |
||||
|
if (opts.enabled !== undefined) conditions.push(eq(scheduledTasks.enabled, opts.enabled)); |
||||
|
|
||||
|
const where = conditions.length > 0 ? and(...conditions) : undefined; |
||||
|
|
||||
|
const [rows, countResult] = await Promise.all([ |
||||
|
dbGlobal |
||||
|
.select() |
||||
|
.from(scheduledTasks) |
||||
|
.where(where) |
||||
|
.orderBy(desc(scheduledTasks.createdAt)) |
||||
|
.limit(pageSize) |
||||
|
.offset((page - 1) * pageSize), |
||||
|
dbGlobal |
||||
|
.select({ count: sql<number>`count(*)` }) |
||||
|
.from(scheduledTasks) |
||||
|
.where(where), |
||||
|
]); |
||||
|
|
||||
|
return { |
||||
|
list: rows, |
||||
|
total: countResult[0]?.count ?? 0, |
||||
|
page, |
||||
|
pageSize, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export async function getTaskById(id: string) { |
||||
|
const rows = await dbGlobal |
||||
|
.select() |
||||
|
.from(scheduledTasks) |
||||
|
.where(eq(scheduledTasks.id, id)) |
||||
|
.limit(1); |
||||
|
return rows[0] ?? null; |
||||
|
} |
||||
|
|
||||
|
export async function createTask(input: CreateTaskInput) { |
||||
|
const id = uuid(); |
||||
|
const ts = now(); |
||||
|
await dbGlobal.insert(scheduledTasks).values({ |
||||
|
id, |
||||
|
...input, |
||||
|
createdAt: ts, |
||||
|
updatedAt: ts, |
||||
|
}); |
||||
|
return getTaskById(id); |
||||
|
} |
||||
|
|
||||
|
export async function updateTask(id: string, input: UpdateTaskInput) { |
||||
|
await dbGlobal |
||||
|
.update(scheduledTasks) |
||||
|
.set({ ...input, updatedAt: now() }) |
||||
|
.where(eq(scheduledTasks.id, id)); |
||||
|
return getTaskById(id); |
||||
|
} |
||||
|
|
||||
|
export async function deleteTask(id: string) { |
||||
|
await dbGlobal.delete(scheduledTasks).where(eq(scheduledTasks.id, id)); |
||||
|
} |
||||
|
|
||||
|
export async function toggleTask(id: string, enabled: boolean) { |
||||
|
await dbGlobal |
||||
|
.update(scheduledTasks) |
||||
|
.set({ enabled: enabled ? 1 : 0, updatedAt: now() }) |
||||
|
.where(eq(scheduledTasks.id, id)); |
||||
|
return getTaskById(id); |
||||
|
} |
||||
|
|
||||
|
// ---- Execution logs ----
|
||||
|
|
||||
|
export async function listExecutions(opts: { |
||||
|
taskId?: string; |
||||
|
status?: string; |
||||
|
page?: number; |
||||
|
pageSize?: number; |
||||
|
}) { |
||||
|
const page = opts.page ?? 1; |
||||
|
const pageSize = opts.pageSize ?? 20; |
||||
|
const conditions = []; |
||||
|
|
||||
|
if (opts.taskId) conditions.push(eq(taskExecutionLogs.taskId, opts.taskId)); |
||||
|
if (opts.status) conditions.push(eq(taskExecutionLogs.status, opts.status)); |
||||
|
|
||||
|
const where = conditions.length > 0 ? and(...conditions) : undefined; |
||||
|
|
||||
|
const [rows, countResult] = await Promise.all([ |
||||
|
dbGlobal |
||||
|
.select() |
||||
|
.from(taskExecutionLogs) |
||||
|
.where(where) |
||||
|
.orderBy(desc(taskExecutionLogs.startedAt)) |
||||
|
.limit(pageSize) |
||||
|
.offset((page - 1) * pageSize), |
||||
|
dbGlobal |
||||
|
.select({ count: sql<number>`count(*)` }) |
||||
|
.from(taskExecutionLogs) |
||||
|
.where(where), |
||||
|
]); |
||||
|
|
||||
|
return { |
||||
|
list: rows, |
||||
|
total: countResult[0]?.count ?? 0, |
||||
|
page, |
||||
|
pageSize, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export async function getRecentExecutions(taskId: string, limit = 10) { |
||||
|
return dbGlobal |
||||
|
.select() |
||||
|
.from(taskExecutionLogs) |
||||
|
.where(eq(taskExecutionLogs.taskId, taskId)) |
||||
|
.orderBy(desc(taskExecutionLogs.startedAt)) |
||||
|
.limit(limit); |
||||
|
} |
||||
|
|
||||
|
export async function createExecutionLog(input: { |
||||
|
taskId: string; |
||||
|
status: "running" | "success" | "failed"; |
||||
|
errorMessage?: string; |
||||
|
resultSummary?: string; |
||||
|
}) { |
||||
|
const id = uuid(); |
||||
|
await dbGlobal.insert(taskExecutionLogs).values({ |
||||
|
id, |
||||
|
...input, |
||||
|
startedAt: now(), |
||||
|
}); |
||||
|
return id; |
||||
|
} |
||||
|
|
||||
|
export async function updateExecutionLog( |
||||
|
id: string, |
||||
|
input: { status: "success" | "failed"; errorMessage?: string; resultSummary?: string } |
||||
|
) { |
||||
|
await dbGlobal |
||||
|
.update(taskExecutionLogs) |
||||
|
.set({ ...input, finishedAt: now() }) |
||||
|
.where(eq(taskExecutionLogs.id, id)); |
||||
|
} |
||||
|
|
||||
|
export async function markStaleRunningTasks() { |
||||
|
const result = await dbGlobal |
||||
|
.update(taskExecutionLogs) |
||||
|
.set({ |
||||
|
status: "failed", |
||||
|
errorMessage: "Service restarted — task was interrupted", |
||||
|
finishedAt: now(), |
||||
|
}) |
||||
|
.where(eq(taskExecutionLogs.status, "running")); |
||||
|
|
||||
|
if (result.rowsAffected > 0) { |
||||
|
logger.info("Marked %d stale running execution(s) as failed", result.rowsAffected); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export async function cleanupOldLogs(retentionDays: number) { |
||||
|
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000); |
||||
|
const result = await dbGlobal |
||||
|
.delete(taskExecutionLogs) |
||||
|
.where( |
||||
|
and( |
||||
|
eq(taskExecutionLogs.status, "success"), |
||||
|
sql`${taskExecutionLogs.startedAt} < ${cutoff.getTime()}` |
||||
|
) |
||||
|
); |
||||
|
|
||||
|
if (result.rowsAffected > 0) { |
||||
|
logger.info("Cleaned up %d old execution log(s)", result.rowsAffected); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export async function getStats() { |
||||
|
const [totalTasks, enabledTasks, recentExecutions] = await Promise.all([ |
||||
|
dbGlobal.select({ count: sql<number>`count(*)` }).from(scheduledTasks), |
||||
|
dbGlobal |
||||
|
.select({ count: sql<number>`count(*)` }) |
||||
|
.from(scheduledTasks) |
||||
|
.where(eq(scheduledTasks.enabled, 1)), |
||||
|
dbGlobal |
||||
|
.select({ count: sql<number>`count(*)` }) |
||||
|
.from(taskExecutionLogs) |
||||
|
.where( |
||||
|
sql`${taskExecutionLogs.startedAt} > ${Date.now() - 24 * 60 * 60 * 1000}` |
||||
|
), |
||||
|
]); |
||||
|
|
||||
|
return { |
||||
|
totalTasks: totalTasks[0]?.count ?? 0, |
||||
|
enabledTasks: enabledTasks[0]?.count ?? 0, |
||||
|
last24hExecutions: recentExecutions[0]?.count ?? 0, |
||||
|
}; |
||||
|
} |
||||
Loading…
Reference in new issue