feat: Optimize server availability checks with parallelized TCP connections and in-memory caching, and update Dashboard to handle paginated API responses for improved performance and user experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 6m47s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 6m47s
This commit is contained in:
+49
-22
@@ -1397,36 +1397,59 @@ app.get('/api/auto-urls', async (req, res) => {
|
||||
});
|
||||
|
||||
// --- Servers availability check ---
|
||||
// Simple TCP connect check to 80 then 443 with short timeouts
|
||||
// Optimized TCP check: parallelize ports and hosts, cap per-host time, add in-memory TTL cache
|
||||
function tcpCheck(host, port, timeoutMs) {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket();
|
||||
let settled = false;
|
||||
const onOk = () => { if (!settled) { settled = true; try{ socket.destroy(); }catch{} resolve(true); } };
|
||||
const onFail = () => { if (!settled) { settled = true; try{ socket.destroy(); }catch{} resolve(false); } };
|
||||
socket.setTimeout(timeoutMs);
|
||||
socket.once('error', onFail);
|
||||
socket.once('timeout', onFail);
|
||||
socket.connect(port, host, onOk);
|
||||
const settle = (ok) => { if (!settled) { settled = true; try { socket.destroy(); } catch {} resolve(ok); } };
|
||||
socket.setTimeout(timeoutMs, () => settle(false));
|
||||
socket.once('error', () => settle(false));
|
||||
socket.connect(port, host, () => settle(true));
|
||||
});
|
||||
}
|
||||
|
||||
async function checkOneServer(srv, timeoutMs = 1500) {
|
||||
const hostCandidates = [];
|
||||
if (srv.ip) hostCandidates.push(srv.ip);
|
||||
if (srv.dns) hostCandidates.push(srv.dns);
|
||||
for (const host of hostCandidates) {
|
||||
// try 80 then 443
|
||||
const ok80 = await tcpCheck(host, 80, timeoutMs);
|
||||
if (ok80) return true;
|
||||
const ok443 = await tcpCheck(host, 443, timeoutMs);
|
||||
if (ok443) return true;
|
||||
}
|
||||
return false;
|
||||
function anyTrue(promises) {
|
||||
return new Promise((resolve) => {
|
||||
if (!Array.isArray(promises) || promises.length === 0) return resolve(false);
|
||||
let remaining = promises.length;
|
||||
let resolved = false;
|
||||
for (const p of promises) {
|
||||
Promise.resolve(p).then((v) => {
|
||||
if (v && !resolved) { resolved = true; resolve(true); }
|
||||
}).finally(() => {
|
||||
remaining -= 1;
|
||||
if (remaining === 0 && !resolved) resolve(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function checkOneServerFast(srv, perSocketTimeoutMs = 800, perServerBudgetMs = 1000) {
|
||||
const hosts = [];
|
||||
if (srv.ip) hosts.push(String(srv.ip));
|
||||
if (srv.dns) hosts.push(String(srv.dns));
|
||||
const tryOneHost = (host) => anyTrue([
|
||||
// try common ports simultaneously
|
||||
tcpCheck(host, 443, perSocketTimeoutMs),
|
||||
tcpCheck(host, 80, perSocketTimeoutMs),
|
||||
]);
|
||||
const run = anyTrue(hosts.map((h) => tryOneHost(h)));
|
||||
// Per-server overall budget
|
||||
const timeout = new Promise((resolve) => setTimeout(() => resolve(false), perServerBudgetMs));
|
||||
return Promise.race([run, timeout]);
|
||||
}
|
||||
|
||||
const availabilityCache = { at: 0, data: null };
|
||||
|
||||
app.get('/api/servers/availability', async (req, res) => {
|
||||
try {
|
||||
const ttlSeconds = Math.max(0, Math.min(300, Number(req.query.ttlSeconds) || 30));
|
||||
const now = Date.now();
|
||||
if (availabilityCache.data && (now - availabilityCache.at) < ttlSeconds * 1000) {
|
||||
return res.json({ ...availabilityCache.data, cached: true });
|
||||
}
|
||||
|
||||
const data = await s3.getObject({ Bucket: BUCKET_NAME, Key: 'servers.json' }).promise();
|
||||
let servers = [];
|
||||
try {
|
||||
@@ -1435,10 +1458,14 @@ app.get('/api/servers/availability', async (req, res) => {
|
||||
} catch {
|
||||
servers = [];
|
||||
}
|
||||
const checks = await Promise.allSettled(servers.map(s => checkOneServer(s)));
|
||||
|
||||
const checks = await Promise.allSettled(servers.map((s) => checkOneServerFast(s)));
|
||||
const statuses = servers.map((s, i) => ({ ip: s.ip, dns: s.dns, online: checks[i].status === 'fulfilled' ? Boolean(checks[i].value) : false }));
|
||||
const online = statuses.filter(x => x.online).length;
|
||||
res.json({ online, total: servers.length, statuses });
|
||||
const online = statuses.filter((x) => x.online).length;
|
||||
const payload = { online, total: servers.length, statuses };
|
||||
availabilityCache.at = Date.now();
|
||||
availabilityCache.data = payload;
|
||||
res.json(payload);
|
||||
} catch (e) {
|
||||
console.error('availability error', e);
|
||||
res.status(500).json({ online: 0, total: 0, statuses: [] });
|
||||
|
||||
+26
-10
@@ -80,11 +80,11 @@ function Dashboard() {
|
||||
try {
|
||||
// Загружаем данные с обработкой ошибок для каждого endpoint
|
||||
const results = await Promise.allSettled([
|
||||
axios.get('/api/domains-new'),
|
||||
axios.get('/api/ip-ranges'),
|
||||
axios.get('/api/asns'),
|
||||
axios.get('/api/domains-new', { params: { offset: 0, limit: 1 } }),
|
||||
axios.get('/api/ip-ranges', { params: { offset: 0, limit: 1 } }),
|
||||
axios.get('/api/asns', { params: { offset: 0, limit: 1 } }),
|
||||
axios.get('/api/servers'),
|
||||
axios.get('/api/servers/availability'),
|
||||
axios.get('/api/servers/availability', { params: { ttlSeconds: 60 } }),
|
||||
axios.get('/api/s3/last-modified')
|
||||
]);
|
||||
|
||||
@@ -97,9 +97,15 @@ function Dashboard() {
|
||||
const s3Res = results[5];
|
||||
|
||||
// Получаем данные серверов для подсчета дополнительной статистики
|
||||
const domains = domainsRes.status === 'fulfilled' ? domainsRes.value.data : [];
|
||||
const ipRanges = ipRangesRes.status === 'fulfilled' ? ipRangesRes.value.data : [];
|
||||
const asns = asnsRes.status === 'fulfilled' ? asnsRes.value.data : [];
|
||||
const domains = domainsRes.status === 'fulfilled'
|
||||
? (Array.isArray(domainsRes.value.data?.items) ? domainsRes.value.data.items : domainsRes.value.data)
|
||||
: [];
|
||||
const ipRanges = ipRangesRes.status === 'fulfilled'
|
||||
? (Array.isArray(ipRangesRes.value.data?.items) ? ipRangesRes.value.data.items : ipRangesRes.value.data)
|
||||
: [];
|
||||
const asns = asnsRes.status === 'fulfilled'
|
||||
? (Array.isArray(asnsRes.value.data?.items) ? asnsRes.value.data.items : asnsRes.value.data)
|
||||
: [];
|
||||
const servers = serversRes.status === 'fulfilled' ? serversRes.value.data : [];
|
||||
setRaw({ domains, ipRanges, asns, servers });
|
||||
const countries = new Set(servers.map(server => server.country).filter(Boolean));
|
||||
@@ -111,10 +117,20 @@ function Dashboard() {
|
||||
const lmRaw = s3Res.status === 'fulfilled' ? s3Res.value.data?.domainsNew?.lastModified : null;
|
||||
const lastModified = lmRaw ? new Date(lmRaw).toLocaleString() : new Date().toLocaleString();
|
||||
|
||||
const domainsCount = domainsRes.status === 'fulfilled' && typeof domainsRes.value.data?.total === 'number'
|
||||
? domainsRes.value.data.total
|
||||
: domains.length;
|
||||
const ipRangesCount = ipRangesRes.status === 'fulfilled' && typeof ipRangesRes.value.data?.total === 'number'
|
||||
? ipRangesRes.value.data.total
|
||||
: ipRanges.length;
|
||||
const asnsCount = asnsRes.status === 'fulfilled' && typeof asnsRes.value.data?.total === 'number'
|
||||
? asnsRes.value.data.total
|
||||
: asns.length;
|
||||
|
||||
setStats({
|
||||
domainsCount: domains.length,
|
||||
ipRangesCount: ipRanges.length,
|
||||
asnsCount: asns.length,
|
||||
domainsCount,
|
||||
ipRangesCount,
|
||||
asnsCount,
|
||||
serversCount: servers.length,
|
||||
lastModified,
|
||||
countriesCount: countries.size,
|
||||
|
||||
Reference in New Issue
Block a user