smart-stick-loadbalancer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -0
- package/package.json +30 -0
- package/src/config.json +28 -0
- package/src/healthChecker.js +32 -0
- package/src/index.js +108 -0
- package/src/notifier.js +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Smart Stick Load Balancer
|
|
2
|
+
|
|
3
|
+
A lightweight **sticky-session load balancer** for Node.js. It distributes requests to multiple backends, supports sticky sessions via cookies, performs health checks, and can optionally send email alerts.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Sticky sessions via cookies
|
|
10
|
+
- Health checks with automatic failover
|
|
11
|
+
- Optional email alerts
|
|
12
|
+
- WebSocket and HTTP support
|
|
13
|
+
- Minimal setup
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install smart-stick-loadbalancer
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Example Configuration (`config.json`)
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"port": 3001,
|
|
30
|
+
"backends": [
|
|
31
|
+
{ "id": 0, "url": "http://localhost:5000", "owner": "example@example.com", "weight": 1 },
|
|
32
|
+
{ "id": 1, "url": "http://localhost:5001", "owner": "example@example.com", "weight": 1 }
|
|
33
|
+
],
|
|
34
|
+
"health": { "interval": 10000, "timeout": 2000 },
|
|
35
|
+
"email": {
|
|
36
|
+
"service": "gmail",
|
|
37
|
+
"auth": { "user": "<your-email@gmail.com>", "pass": "<your-app-password>" }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
const { createStickyProxy } = require('smart-stick-loadbalancer');
|
|
48
|
+
const config = require('./config.json');
|
|
49
|
+
|
|
50
|
+
const lb = createStickyProxy(config);
|
|
51
|
+
lb.start(); // starts the load balancer
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- Requests to `http://localhost:3001` will be forwarded to the backends.
|
|
55
|
+
- Sticky sessions are automatically managed using cookies.
|
|
56
|
+
- Health checks run periodically and remove unhealthy backends from rotation.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Health Endpoint
|
|
61
|
+
|
|
62
|
+
```http
|
|
63
|
+
GET /_lb/health
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Response example:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"timestamp": "2025-12-28T12:00:00.000Z",
|
|
71
|
+
"total": 2,
|
|
72
|
+
"healthy": 2,
|
|
73
|
+
"unhealthy": 0,
|
|
74
|
+
"backends": [
|
|
75
|
+
{ "id": 0, "url": "http://localhost:5000", "healthy": true, "requests": 5 },
|
|
76
|
+
{ "id": 1, "url": "http://localhost:5001", "healthy": true, "requests": 3 }
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Email Alerts
|
|
84
|
+
|
|
85
|
+
- Alerts are sent when a backend goes down or comes back up.
|
|
86
|
+
- Requires valid Gmail credentials (or any supported email service).
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "smart-stick-loadbalancer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight sticky-session load balancer for Node.js with health checks and optional email alerts.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"smart-stick-lb": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"load-balancer",
|
|
14
|
+
"sticky-session",
|
|
15
|
+
"proxy",
|
|
16
|
+
"Node.js",
|
|
17
|
+
"http-proxy",
|
|
18
|
+
"health-check"
|
|
19
|
+
],
|
|
20
|
+
"author": "SWAYAM GUPTA",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"axios": "^1.10.0",
|
|
24
|
+
"cookie-parser": "^1.4.7",
|
|
25
|
+
"express": "^5.1.0",
|
|
26
|
+
"http-proxy-middleware": "^3.0.5",
|
|
27
|
+
"nodemailer": "^7.0.5",
|
|
28
|
+
"socket.io": "^4.8.3"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/config.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"port": 3001,
|
|
3
|
+
"backends": [
|
|
4
|
+
{
|
|
5
|
+
"id": 0,
|
|
6
|
+
"url":"<link to reviewer instance>",
|
|
7
|
+
"owner": "<reviewer email>",
|
|
8
|
+
"weight": 1
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": 1,
|
|
12
|
+
"url": "<link to reviewer instance>",
|
|
13
|
+
"owner": "<reviewer email>",
|
|
14
|
+
"weight": 1
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"health": {
|
|
18
|
+
"interval": 10000,
|
|
19
|
+
"timeout": 2000
|
|
20
|
+
},
|
|
21
|
+
"email": {
|
|
22
|
+
"service": "gmail",
|
|
23
|
+
"auth": {
|
|
24
|
+
"user": "<sendermail>@gmail.com",
|
|
25
|
+
"pass": "<app password>"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const axios = require("axios");
|
|
2
|
+
|
|
3
|
+
async function checkHealth(server, timeout = 2000) {
|
|
4
|
+
try {
|
|
5
|
+
await axios.get(server.url, { timeout });
|
|
6
|
+
return true;
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function startHealthChecker(servers, config, onDown, onUp) {
|
|
13
|
+
const lastStatus = new Map();
|
|
14
|
+
const interval = config?.interval || 10000;
|
|
15
|
+
const timeout = config?.timeout || 2000;
|
|
16
|
+
|
|
17
|
+
setInterval(async () => {
|
|
18
|
+
for (const server of servers) {
|
|
19
|
+
const isUp = await checkHealth(server, timeout);
|
|
20
|
+
const wasUp = lastStatus.get(server.url) ?? true;
|
|
21
|
+
server.healthy = isUp;
|
|
22
|
+
server.lastChecked = new Date().toISOString();
|
|
23
|
+
|
|
24
|
+
if (!isUp && wasUp && onDown) onDown(server);
|
|
25
|
+
if (isUp && !wasUp && onUp) onUp(server);
|
|
26
|
+
|
|
27
|
+
lastStatus.set(server.url, isUp);
|
|
28
|
+
}
|
|
29
|
+
}, interval);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { checkHealth, startHealthChecker };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
const http = require("http");
|
|
3
|
+
const { createProxyMiddleware } = require("http-proxy-middleware");
|
|
4
|
+
const cookieParser = require("cookie-parser");
|
|
5
|
+
const socketIo = require("socket.io");
|
|
6
|
+
const { startHealthChecker } = require("./healthChecker");
|
|
7
|
+
const { sendDownAlert, sendUpAlert } = require("./notifier");
|
|
8
|
+
|
|
9
|
+
function createStickyProxy(options = {}) {
|
|
10
|
+
if (!options.port || !options.backends) {
|
|
11
|
+
throw new Error("Port and backends must be provided");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const app = express();
|
|
15
|
+
const server = http.createServer(app);
|
|
16
|
+
const io = socketIo(server);
|
|
17
|
+
|
|
18
|
+
let current = 0;
|
|
19
|
+
const servers = options.backends.map((b) => ({
|
|
20
|
+
...b,
|
|
21
|
+
healthy: true,
|
|
22
|
+
requests: 0,
|
|
23
|
+
lastChecked: null,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
// Middleware
|
|
27
|
+
app.use(cookieParser());
|
|
28
|
+
|
|
29
|
+
// Health endpoint
|
|
30
|
+
app.get("/_lb/health", (req, res) => {
|
|
31
|
+
res.json({
|
|
32
|
+
timestamp: new Date().toISOString(),
|
|
33
|
+
total: servers.length,
|
|
34
|
+
healthy: servers.filter((s) => s.healthy).length,
|
|
35
|
+
unhealthy: servers.filter((s) => !s.healthy).length,
|
|
36
|
+
backends: servers.map((s) => ({
|
|
37
|
+
id: s.id,
|
|
38
|
+
url: s.url,
|
|
39
|
+
healthy: s.healthy,
|
|
40
|
+
requests: s.requests,
|
|
41
|
+
lastChecked: s.lastChecked,
|
|
42
|
+
})),
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Sticky selection logic
|
|
47
|
+
function getHealthyBackend(req, res) {
|
|
48
|
+
const healthyBackends = servers.filter((s) => s.healthy);
|
|
49
|
+
if (!healthyBackends.length) return null;
|
|
50
|
+
|
|
51
|
+
let backendId = parseInt(req.cookies["X-Backend-ID"]);
|
|
52
|
+
if (!isNaN(backendId) && servers[backendId]?.healthy) {
|
|
53
|
+
return servers[backendId];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const selected = healthyBackends[current % healthyBackends.length];
|
|
57
|
+
current++;
|
|
58
|
+
res.cookie("X-Backend-ID", selected.id, { httpOnly: true });
|
|
59
|
+
return selected;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Proxy instances
|
|
63
|
+
const proxies = new Map();
|
|
64
|
+
servers.forEach((backend) => {
|
|
65
|
+
proxies.set(
|
|
66
|
+
backend.id,
|
|
67
|
+
createProxyMiddleware({
|
|
68
|
+
target: backend.url,
|
|
69
|
+
changeOrigin: true,
|
|
70
|
+
ws: true,
|
|
71
|
+
onProxyRes(proxyRes, req, res) {
|
|
72
|
+
res.setHeader("x-backend", backend.url);
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Proxy handler
|
|
79
|
+
app.use((req, res, next) => {
|
|
80
|
+
const backend = getHealthyBackend(req, res);
|
|
81
|
+
if (!backend) return res.status(503).send("All servers down.");
|
|
82
|
+
|
|
83
|
+
backend.requests++;
|
|
84
|
+
io.emit("request", { ip: req.ip, to: backend.url });
|
|
85
|
+
io.emit("update", servers);
|
|
86
|
+
|
|
87
|
+
const proxy = proxies.get(backend.id);
|
|
88
|
+
proxy(req, res, next);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Health checks with optional email alerts
|
|
92
|
+
startHealthChecker(
|
|
93
|
+
servers,
|
|
94
|
+
options.health || { interval: 10000, timeout: 2000 },
|
|
95
|
+
options.email ? (server) => { sendDownAlert(server, options.email); io.emit("update", servers); } : null,
|
|
96
|
+
options.email ? (server) => { sendUpAlert(server, options.email); io.emit("update", servers); } : null
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
function start() {
|
|
100
|
+
server.listen(options.port, () =>
|
|
101
|
+
console.log(`Sticky Proxy running on port ${options.port}`)
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { app, server, io, start, servers };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { createStickyProxy };
|
package/src/notifier.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const nodemailer = require("nodemailer");
|
|
2
|
+
|
|
3
|
+
function createTransport(emailConfig) {
|
|
4
|
+
if (!emailConfig) return null;
|
|
5
|
+
return nodemailer.createTransport({
|
|
6
|
+
service: emailConfig.service,
|
|
7
|
+
auth: emailConfig.auth,
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function sendDownAlert(server, emailConfig) {
|
|
12
|
+
const transporter = createTransport(emailConfig);
|
|
13
|
+
if (!transporter) return;
|
|
14
|
+
transporter.sendMail({
|
|
15
|
+
from: emailConfig.auth.user,
|
|
16
|
+
to: server.owner,
|
|
17
|
+
subject: "⚠️ Backend Down",
|
|
18
|
+
text: `Your backend at ${server.url} is down.`,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sendUpAlert(server, emailConfig) {
|
|
23
|
+
const transporter = createTransport(emailConfig);
|
|
24
|
+
if (!transporter) return;
|
|
25
|
+
transporter.sendMail({
|
|
26
|
+
from: emailConfig.auth.user,
|
|
27
|
+
to: server.owner,
|
|
28
|
+
subject: "✅ Backend Recovered",
|
|
29
|
+
text: `Your backend at ${server.url} is back online.`,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { sendDownAlert, sendUpAlert };
|