kv-music/js/instance-health.js

162 lines
5.2 KiB
JavaScript

//instance-health.js
const HEALTH_CHECK_INTERVAL = 5 * 60 * 1000;
const HEALTH_CHECK_TIMEOUT = 8000;
const MAX_CONSECUTIVE_FAILURES = 3;
const INITIAL_DELAY = 10000;
const IS_DEV = typeof window !== 'undefined' && window.location?.hostname === 'localhost';
class InstanceHealthChecker {
constructor() {
this.healthCache = new Map();
this.checkTimers = {};
this.listeners = [];
}
onHealthUpdate(callback) {
this.listeners.push(callback);
}
notifyListeners(instances) {
this.listeners.forEach((cb) => cb(instances));
}
async checkInstance(url, type = 'api') {
const testPath = '/';
const fullUrl = url.endsWith('/') ? `${url}${testPath.substring(1)}` : `${url}${testPath}`;
const startTime = Date.now();
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT);
try {
const response = await fetch(fullUrl, {
method: 'GET',
signal: controller.signal,
});
clearTimeout(timeoutId);
const latency = Date.now() - startTime;
const existing = this.healthCache.get(url) || { url, failures: 0 };
const status = response.ok || response.status === 401 || response.status === 404 ? 'online' : 'degraded';
this.healthCache.set(url, {
...existing,
status,
latency,
lastCheck: Date.now(),
failures: response.ok ? 0 : existing.failures,
type,
});
return { url, status, latency, online: response.ok };
} catch (error) {
clearTimeout(timeoutId);
const existing = this.healthCache.get(url) || { url, failures: 0 };
const newFailures = existing.failures + 1;
const status = newFailures >= MAX_CONSECUTIVE_FAILURES ? 'offline' : 'degraded';
this.healthCache.set(url, {
url,
status,
latency: null,
lastCheck: Date.now(),
failures: newFailures,
error: error.message,
type,
});
return { url, status, latency: null, online: false, error: error.message };
}
}
async checkAll(instances, type = 'api') {
const checks = instances.map((inst) => {
const url = typeof inst === 'string' ? inst : inst.url;
return this.checkInstance(url, type);
});
const results = await Promise.allSettled(checks);
return results.map((r) => r.value || { url: 'unknown', status: 'offline', online: false });
}
getHealthStatus(url) {
return this.healthCache.get(url) || { url, status: 'unknown', latency: null, lastCheck: null };
}
getHealthyInstances(instances, type = 'api') {
return instances.filter((inst) => {
const url = typeof inst === 'string' ? inst : inst.url;
const health = this.healthCache.get(url);
return !health || health.status === 'online' || health.status === 'degraded';
});
}
getHealthyAndSorted(instances, type = 'api') {
const withHealth = instances.map((inst) => {
const url = typeof inst === 'string' ? inst : inst.url;
const health = this.healthCache.get(url);
return {
...inst,
healthStatus: health?.status || 'unknown',
latency: health?.latency ?? null,
};
});
return withHealth.sort((a, b) => {
if (a.healthStatus === 'offline' && b.healthStatus !== 'offline') return 1;
if (b.healthStatus === 'offline' && a.healthStatus !== 'offline') return -1;
if (a.latency !== null && b.latency !== null) return a.latency - b.latency;
if (a.latency !== null) return -1;
if (b.latency !== null) return 1;
return 0;
});
}
startPeriodicCheck(instancesGetter, type = 'api') {
if (this.checkTimers[type]) {
clearInterval(this.checkTimers[type]);
}
if (IS_DEV) {
console.log(`Health check disabled in dev mode for ${type}`);
return;
}
const check = async () => {
const instances = await instancesGetter(type);
if (!instances || instances.length === 0) return;
const results = await this.checkAll(instances, type);
this.notifyListeners(results);
};
setTimeout(check, INITIAL_DELAY);
this.checkTimers[type] = setInterval(check, HEALTH_CHECK_INTERVAL);
}
stopPeriodicCheck(type = 'api') {
if (this.checkTimers[type]) {
clearInterval(this.checkTimers[type]);
delete this.checkTimers[type];
}
}
stopAllChecks() {
Object.keys(this.checkTimers).forEach((type) => this.stopPeriodicCheck(type));
}
async quickCheck(url, type = 'api') {
return this.checkInstance(url, type);
}
getCachedStatus(url) {
return this.healthCache.get(url);
}
clearCache() {
this.healthCache.clear();
}
}
export const instanceHealth = new InstanceHealthChecker();