Skip to content

Commit ccbd1e3

Browse files
committed
chore: continuing with chapter 12
1 parent 1c57cea commit ccbd1e3

File tree

6 files changed

+779
-0
lines changed

6 files changed

+779
-0
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# 06-http-dynamic-load-balancer
2+
3+
This example demonstrates how to implement a dynamic load balancer in Node.js
4+
using consul as a service discovery mechanism.
5+
6+
## Dependencies
7+
8+
This example requires you to install some third-party dependencies from npm.
9+
10+
If you have `pnpm` installed, you can do that with:
11+
12+
```bash
13+
pnpm install
14+
```
15+
16+
Alternatively, if you prefer to use another package manager, make sure to delete
17+
the `pnpm-lock.yaml` file before using it.
18+
19+
If you want to use `npm`, you can run:
20+
21+
```bash
22+
npm install
23+
```
24+
25+
If you want to use `yarn`, you can run:
26+
27+
```bash
28+
yarn install
29+
```
30+
31+
You will also need to install Consul following the
32+
[instructions for your system](https://nodejsdp.link/consul-install), or by
33+
running:
34+
35+
```bash
36+
sudo apt-get install consul # on debian / ubuntu based systems
37+
# or
38+
brew install consul # on mac with brew installed
39+
```
40+
41+
## Run
42+
43+
Start the Consul service registry locally:
44+
45+
```bash
46+
consul agent -dev
47+
```
48+
49+
Start the load balancer:
50+
51+
```bash
52+
node loadBalancer.js
53+
```
54+
55+
If you try to access a service before starting any servers, you'll get a 502
56+
error:
57+
58+
```bash
59+
curl localhost:8080/api
60+
# Output: Bad Gateway
61+
```
62+
63+
In separate terminals, start your service instances (e.g., two api-service and
64+
one webapp-service):
65+
66+
```bash
67+
app.js api-service
68+
app.js api-service
69+
app.js webapp-service
70+
```
71+
72+
Now, requests to the load balancer will be distributed among the running
73+
services:
74+
75+
```bash
76+
curl localhost:8080/api
77+
# Output: api-service response from <port>
78+
```
79+
80+
Run the curl command multiple times to see responses from different servers,
81+
confirming load balancing is working.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { randomUUID } from 'node:crypto'
2+
import { createServer } from 'node:http'
3+
import portfinder from 'portfinder' // v1.0.37
4+
import { ConsulClient } from './consul.js'
5+
6+
const serviceType = process.argv[2]
7+
if (!serviceType) {
8+
console.error('Usage: node app.js <service-type>')
9+
process.exit(1)
10+
}
11+
12+
const consulClient = new ConsulClient()
13+
14+
const port = await portfinder.getPort()
15+
const address = process.env.ADDRESS || 'localhost'
16+
const serviceId = randomUUID()
17+
18+
async function registerService() {
19+
await consulClient.registerService({
20+
id: serviceId,
21+
name: serviceType,
22+
address,
23+
port,
24+
tags: [serviceType],
25+
})
26+
27+
console.log(
28+
`${serviceType} registered successfully as ${serviceId} on ${address}:${port}`
29+
)
30+
}
31+
32+
async function unregisterService(err) {
33+
err && console.error(err)
34+
console.log(`deregistering ${serviceId}`)
35+
try {
36+
await consulClient.deregisterService(serviceId)
37+
} catch (deregisterError) {
38+
console.error(`Failed to deregister service: ${deregisterError.message}`)
39+
}
40+
process.exit(err ? 1 : 0)
41+
}
42+
43+
process.on('uncaughtException', unregisterService)
44+
process.on('SIGINT', unregisterService)
45+
46+
const server = createServer((_req, res) => {
47+
// Simulate some processing time
48+
let i = 1e7
49+
while (i > 0) {
50+
i--
51+
}
52+
console.log(`Handling request from ${process.pid}`)
53+
res.end(`${serviceType} response from ${process.pid}\n`)
54+
})
55+
56+
server.listen(port, address, async () => {
57+
console.log(`Started ${serviceType} on port ${port} with PID ${process.pid}`)
58+
await registerService()
59+
})
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
export class ConsulClient {
2+
constructor(baseUrl = 'http://localhost:8500') {
3+
this.baseUrl = baseUrl
4+
}
5+
6+
async registerService({ name, id, address, port, tags = [] }) {
7+
const url = `${this.baseUrl}/v1/agent/service/register`
8+
const body = {
9+
// biome-ignore lint/style/useNamingConvention: Consul API
10+
Name: name,
11+
// biome-ignore lint/style/useNamingConvention: Consul API
12+
ID: id,
13+
// biome-ignore lint/style/useNamingConvention: Consul API
14+
Address: address,
15+
// biome-ignore lint/style/useNamingConvention: Consul API
16+
Port: port,
17+
// biome-ignore lint/style/useNamingConvention: Consul API
18+
Tags: tags,
19+
}
20+
21+
const response = await fetch(url, {
22+
method: 'PUT',
23+
headers: { 'Content-Type': 'application/json' },
24+
body: JSON.stringify(body),
25+
})
26+
27+
if (!response.ok) {
28+
const responseText = await response.text() // Read the response body to get more details
29+
throw new Error(
30+
`Failed to register service: ${response.statusText}\n${responseText}`
31+
)
32+
}
33+
}
34+
35+
async deregisterService(serviceId) {
36+
const url = `${this.baseUrl}/v1/agent/service/deregister/${serviceId}`
37+
const response = await fetch(url, { method: 'PUT' })
38+
39+
if (!response.ok) {
40+
const responseText = await response.text()
41+
throw new Error(
42+
`Failed to deregister service: ${response.statusText}\n${responseText}`
43+
)
44+
}
45+
}
46+
47+
async getAllServices() {
48+
const url = `${this.baseUrl}/v1/agent/services`
49+
const response = await fetch(url)
50+
51+
if (!response.ok) {
52+
const responseText = await response.text()
53+
throw new Error(
54+
`Failed to get all services: ${response.statusText}\n${responseText}`
55+
)
56+
}
57+
58+
return response.json()
59+
}
60+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { createServer } from 'node:http'
2+
import { createProxyServer } from 'httpxy' // v0.1.7
3+
import { ConsulClient } from './consul.js'
4+
5+
const routing = [
6+
{
7+
path: '/api',
8+
service: 'api-service',
9+
index: 0,
10+
},
11+
{
12+
path: '/',
13+
service: 'webapp-service',
14+
index: 0,
15+
},
16+
]
17+
18+
const consulClient = new ConsulClient()
19+
const proxy = createProxyServer()
20+
21+
const server = createServer(async (req, res) => {
22+
const route = routing.find(route => req.url.startsWith(route.path))
23+
24+
try {
25+
const services = await consulClient.getAllServices()
26+
const servers = Object.values(services).filter(service =>
27+
service.Tags.includes(route.service)
28+
)
29+
30+
if (servers.length > 0) {
31+
route.index = (route.index + 1) % servers.length
32+
const server = servers[route.index]
33+
const target = `http://${server.Address}:${server.Port}`
34+
proxy.web(req, res, { target })
35+
return
36+
}
37+
} catch (err) {
38+
console.error(err)
39+
}
40+
41+
// if servers not found or error occurs
42+
res.writeHead(502)
43+
return res.end('Bad gateway')
44+
})
45+
46+
server.listen(8080, () => {
47+
console.log('Load balancer started on port 8080')
48+
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "06-http-dynamic-load-balancer",
3+
"version": "1.0.0",
4+
"description": "This example demonstrates how to implement a dynamic load balancer in Node.js using consul as a service discovery mechanism",
5+
"type": "module",
6+
"scripts": {},
7+
"engines": {
8+
"node": ">=24"
9+
},
10+
"engineStrict": true,
11+
"keywords": [],
12+
"author": "Luciano Mammino and Mario Casciaro",
13+
"license": "MIT",
14+
"devDependencies": {
15+
"autocannon": "^8.0.0"
16+
},
17+
"dependencies": {
18+
"httpxy": "^0.1.7",
19+
"portfinder": "^1.0.37"
20+
}
21+
}

0 commit comments

Comments
 (0)