Files
anonymous_github/src/queue/queueMetrics.ts
T

121 lines
3.0 KiB
TypeScript

import { createClient, RedisClientType } from "redis";
import config from "../config";
const KEY_PREFIX = "qmetrics";
const TTL_SECONDS = 7 * 24 * 3600 + 3600;
let client: RedisClientType | null = null;
let disabled = false;
function getClient(): RedisClientType | null {
if (disabled) return null;
if (client) return client;
try {
client = createClient({
socket: {
host: config.REDIS_HOSTNAME,
port: config.REDIS_PORT,
reconnectStrategy: false,
},
}) as RedisClientType;
client.on("error", () => {
disabled = true;
if (client?.isOpen) client.destroy();
client = null;
});
client.connect().catch(() => {
disabled = true;
client = null;
});
return client;
} catch {
disabled = true;
return null;
}
}
function minuteTs(now?: number): number {
return Math.floor((now || Date.now()) / 60000) * 60000;
}
function key(queue: string, ts: number): string {
return `${KEY_PREFIX}:${queue}:${ts}`;
}
export async function recordMetric(
queue: string,
type: "completed" | "failed",
durationMs: number
): Promise<void> {
const c = getClient();
if (!c || !c.isOpen) return;
const k = key(queue, minuteTs());
const field = type === "completed" ? "c" : "f";
try {
const pipe = c.multi();
pipe.hIncrBy(k, field, 1);
pipe.hIncrBy(k, "ms", Math.round(Math.max(0, durationMs)));
pipe.expire(k, TTL_SECONDS);
await pipe.exec();
} catch {
// non-critical, don't crash workers
}
}
export interface MetricPoint {
ts: number;
completed: number;
failed: number;
avgMs: number;
}
const METRIC_FIELDS = ["c", "f", "ms"];
const metricsCache = new Map<string, { data: MetricPoint[]; ts: number }>();
const METRICS_CACHE_TTL = 30_000;
export async function queryMetrics(
queue: string,
rangeMinutes: number
): Promise<MetricPoint[]> {
const cacheKey = `${queue}:${rangeMinutes}`;
const cached = metricsCache.get(cacheKey);
if (cached && Date.now() - cached.ts < METRICS_CACHE_TTL) return cached.data;
const c = getClient();
if (!c || !c.isOpen) return [];
const now = minuteTs();
const start = now - (rangeMinutes - 1) * 60000;
const keys: string[] = [];
const timestamps: number[] = [];
for (let t = start; t <= now; t += 60000) {
keys.push(key(queue, t));
timestamps.push(t);
}
try {
const pipe = c.multi();
for (const k of keys) pipe.hmGet(k, METRIC_FIELDS);
const results = await pipe.exec();
const points = timestamps.map((ts, i) => {
const vals = (results[i] as unknown as (string | null)[]) || [];
const completed = parseInt(vals[0] || "0", 10) || 0;
const failed = parseInt(vals[1] || "0", 10) || 0;
const totalMs = parseInt(vals[2] || "0", 10) || 0;
const total = completed + failed;
return {
ts,
completed,
failed,
avgMs: total > 0 ? Math.round(totalMs / total) : 0,
};
});
metricsCache.set(cacheKey, { data: points, ts: Date.now() });
return points;
} catch {
return [];
}
}