fix: Replace SSE with polling for WebSocket status to prevent connection pool exhaustion

- Replace persistent SSE connections with lightweight polling (10s interval)
- Optimize WebSocket status fetching using bulk endpoint instead of N individual calls
- Fix N+1 query problem in /dashboard/hosts endpoint (39 queries → 4 queries)
- Increase database connection pool limit from 5 to 50 via environment variables
- Increase Axios timeout from 10s to 30s for complex operations
- Fix malformed WebSocket routes causing 404 on bulk status endpoint

Fixes timeout issues when adding hosts with multiple WebSocket agents connected.
Reduces database connections from 19 persistent SSE + retries to 1 poll every 10 seconds.
This commit is contained in:
Muhammad Ibrahim
2025-10-28 15:33:55 +00:00
parent ae6afb0ef4
commit 4b6f19c28e
3 changed files with 127 additions and 128 deletions

View File

@@ -193,11 +193,16 @@ router.get(
},
);
// Get hosts with their update status
// Get hosts with their update status - OPTIMIZED
router.get("/hosts", authenticateToken, requireViewHosts, async (_req, res) => {
try {
// Get settings once (outside the loop)
const settings = await prisma.settings.findFirst();
const updateIntervalMinutes = settings?.update_interval || 60;
const thresholdMinutes = updateIntervalMinutes * 2;
// Fetch hosts with groups
const hosts = await prisma.hosts.findMany({
// Show all hosts regardless of status
select: {
id: true,
machine_id: true,
@@ -223,61 +228,65 @@ router.get("/hosts", authenticateToken, requireViewHosts, async (_req, res) => {
},
},
},
_count: {
select: {
host_packages: {
where: {
needs_update: true,
},
},
},
},
},
orderBy: { last_update: "desc" },
});
// Get update counts for each host separately
const hostsWithUpdateInfo = await Promise.all(
hosts.map(async (host) => {
const updatesCount = await prisma.host_packages.count({
where: {
host_id: host.id,
needs_update: true,
},
});
// OPTIMIZATION: Get all package counts in 2 batch queries instead of N*2 queries
const hostIds = hosts.map((h) => h.id);
// Get total packages count for this host
const totalPackagesCount = await prisma.host_packages.count({
where: {
host_id: host.id,
},
});
// Get the agent update interval setting for stale calculation
const settings = await prisma.settings.findFirst();
const updateIntervalMinutes = settings?.update_interval || 60;
const thresholdMinutes = updateIntervalMinutes * 2;
// Calculate effective status based on reporting interval
const isStale = moment(host.last_update).isBefore(
moment().subtract(thresholdMinutes, "minutes"),
);
let effectiveStatus = host.status;
// Override status if host hasn't reported within threshold
if (isStale && host.status === "active") {
effectiveStatus = "inactive";
}
return {
...host,
updatesCount,
totalPackagesCount,
isStale,
effectiveStatus,
};
const [updateCounts, totalCounts] = await Promise.all([
// Get update counts for all hosts at once
prisma.host_packages.groupBy({
by: ["host_id"],
where: {
host_id: { in: hostIds },
needs_update: true,
},
_count: { id: true },
}),
// Get total counts for all hosts at once
prisma.host_packages.groupBy({
by: ["host_id"],
where: {
host_id: { in: hostIds },
},
_count: { id: true },
}),
]);
// Create lookup maps for O(1) access
const updateCountMap = new Map(
updateCounts.map((item) => [item.host_id, item._count.id]),
);
const totalCountMap = new Map(
totalCounts.map((item) => [item.host_id, item._count.id]),
);
// Process hosts with counts from maps (no more DB queries!)
const hostsWithUpdateInfo = hosts.map((host) => {
const updatesCount = updateCountMap.get(host.id) || 0;
const totalPackagesCount = totalCountMap.get(host.id) || 0;
// Calculate effective status based on reporting interval
const isStale = moment(host.last_update).isBefore(
moment().subtract(thresholdMinutes, "minutes"),
);
let effectiveStatus = host.status;
// Override status if host hasn't reported within threshold
if (isStale && host.status === "active") {
effectiveStatus = "inactive";
}
return {
...host,
updatesCount,
totalPackagesCount,
isStale,
effectiveStatus,
};
});
res.json(hostsWithUpdateInfo);
} catch (error) {

View File

@@ -11,7 +11,31 @@ const {
const router = express.Router();
// Get WebSocket connection status by api_id (no database access - pure memory lookup)
// Get WebSocket connection status for multiple hosts at once (bulk endpoint)
router.get("/status", authenticateToken, async (req, res) => {
try {
const { apiIds } = req.query; // Comma-separated list of api_ids
const idArray = apiIds ? apiIds.split(",").filter((id) => id.trim()) : [];
const statusMap = {};
idArray.forEach((apiId) => {
statusMap[apiId] = getConnectionInfo(apiId);
});
res.json({
success: true,
data: statusMap,
});
} catch (error) {
console.error("Error fetching bulk WebSocket status:", error);
res.status(500).json({
success: false,
error: "Failed to fetch WebSocket status",
});
}
});
// Get WebSocket connection status by api_id (single endpoint)
router.get("/status/:apiId", authenticateToken, async (req, res) => {
try {
const { apiId } = req.params;