162 lines
5.2 KiB
JavaScript
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();
|