Skip to content

Commit c6990b0

Browse files
authored
Merge pull request #4 from BackendStack21/extend-load-balancer-strategies
feat: Enhance load balancing strategies
2 parents ba915fd + e3c400b commit c6990b0

10 files changed

Lines changed: 312 additions & 40 deletions

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
- **🔥 Blazing Fast**: Built on Bun - up to 4x faster than Node.js alternatives
1717
- **🎯 Zero Config**: Works out of the box with sensible defaults
18-
- **🧠 Smart Load Balancing**: 5 load balancing algorithms: `round-robin`, `least-connections`, `random`, `weighted`, `ip-hash`
18+
- **🧠 Smart Load Balancing**: Multiple algorithms: `round-robin`, `least-connections`, `random`, `weighted`, `ip-hash`, `p2c` (power-of-two-choices), `latency`, `weighted-least-connections`
1919
- **🛡️ Production Ready**: Circuit breakers, health checks, and auto-failover
2020
- **🔐 Built-in Authentication**: JWT, API keys, JWKS, and OAuth2 support out of the box
2121
- **🎨 Developer Friendly**: Full TypeScript support with intuitive APIs
@@ -107,6 +107,9 @@ console.log('🚀 Bungate running on http://localhost:3000')
107107
- **Least Connections**: Route to the least busy server
108108
- **IP Hash**: Consistent routing based on client IP for session affinity
109109
- **Random**: Randomized distribution for even load
110+
- **Power of Two Choices (p2c)**: Pick the better of two random targets by load/latency
111+
- **Latency**: Prefer the target with the lowest average response time
112+
- **Weighted Least Connections**: Prefer targets with fewer connections normalized by weight
110113
- **Sticky Sessions**: Session affinity with cookie-based persistence
111114

112115
### 🛡️ **Reliability & Resilience**

examples/echo-server-1.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const server = serve({
1919

2020
// Echo endpoint - return request details
2121

22-
// Add random latency delay (0-500ms)
22+
// Add random latency delay (0-200ms)
2323
const delay = Math.floor(Math.random() * 200)
2424
await new Promise((resolve) => setTimeout(resolve, delay))
2525

examples/lb-example-all-options.ts

Lines changed: 126 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const gateway = new BunGateway({
4242
})
4343

4444
// =============================================================================
45-
// 1. ROUND ROBIN LOAD BALANCER WITH HEALTH CHECKS
45+
// ROUND ROBIN LOAD BALANCER WITH HEALTH CHECKS
4646
// =============================================================================
4747
console.log('\n1️⃣ Round Robin with Health Checks')
4848

@@ -83,7 +83,7 @@ gateway.addRoute({
8383
})
8484

8585
// =============================================================================
86-
// 2. WEIGHTED LOAD BALANCER FOR HIGH-PERFORMANCE SCENARIOS
86+
// WEIGHTED LOAD BALANCER FOR HIGH-PERFORMANCE SCENARIOS
8787
// =============================================================================
8888
console.log('2️⃣ Weighted Load Balancer (Performance Optimized)')
8989

@@ -151,7 +151,7 @@ gateway.addRoute({
151151
})
152152

153153
// =============================================================================
154-
// 4. IP HASH FOR SESSION AFFINITY
154+
// IP HASH FOR SESSION AFFINITY
155155
// =============================================================================
156156
console.log('4️⃣ IP Hash for Session Affinity')
157157

@@ -190,7 +190,7 @@ gateway.addRoute({
190190
})
191191

192192
// =============================================================================
193-
// 5. RANDOM STRATEGY WITH ADVANCED ERROR HANDLING
193+
// RANDOM STRATEGY WITH ADVANCED ERROR HANDLING
194194
// =============================================================================
195195
console.log('5️⃣ Random Strategy with Advanced Error Handling')
196196

@@ -296,7 +296,118 @@ gateway.addRoute({
296296
})
297297

298298
// =============================================================================
299-
// 7. MONITORING AND METRICS ENDPOINT
299+
// POWER OF TWO CHOICES (P2C) STRATEGY
300+
// =============================================================================
301+
console.log('6️⃣➕ Power of Two Choices (P2C) Strategy')
302+
303+
gateway.addRoute({
304+
pattern: '/api/p2c/*',
305+
loadBalancer: {
306+
strategy: 'p2c',
307+
targets: [
308+
{ url: 'http://localhost:8080' },
309+
{ url: 'http://localhost:8081' },
310+
],
311+
healthCheck: {
312+
enabled: true,
313+
interval: 10000,
314+
timeout: 4000,
315+
path: '/',
316+
expectedStatus: 200,
317+
},
318+
},
319+
proxy: {
320+
pathRewrite: (path) => path.replace('/api/p2c', ''),
321+
},
322+
hooks: {
323+
beforeRequest: async (req) => {
324+
logger.debug(`🎲 P2C routing for: ${req.url}`)
325+
},
326+
},
327+
})
328+
329+
// Alias for P2C
330+
gateway.addRoute({
331+
pattern: '/api/power-of-two/*',
332+
loadBalancer: {
333+
strategy: 'power-of-two-choices',
334+
targets: [
335+
{ url: 'http://localhost:8080' },
336+
{ url: 'http://localhost:8081' },
337+
],
338+
healthCheck: {
339+
enabled: true,
340+
interval: 10000,
341+
timeout: 4000,
342+
path: '/',
343+
expectedStatus: 200,
344+
},
345+
},
346+
proxy: {
347+
pathRewrite: (path) => path.replace('/api/power-of-two', ''),
348+
},
349+
})
350+
351+
// =============================================================================
352+
// LATENCY-BASED STRATEGY
353+
// =============================================================================
354+
console.log('6️⃣✅ Latency-based Strategy')
355+
356+
gateway.addRoute({
357+
pattern: '/api/latency/*',
358+
loadBalancer: {
359+
strategy: 'latency',
360+
targets: [
361+
{ url: 'http://localhost:8080' },
362+
{ url: 'http://localhost:8081' },
363+
],
364+
healthCheck: {
365+
enabled: true,
366+
interval: 8000,
367+
timeout: 3000,
368+
path: '/',
369+
expectedStatus: 200,
370+
},
371+
},
372+
proxy: {
373+
pathRewrite: (path) => path.replace('/api/latency', ''),
374+
timeout: 10000,
375+
},
376+
hooks: {
377+
afterResponse: async (req, res) => {
378+
logger.info(`⏱️ Latency strategy served with ${res.status}`)
379+
},
380+
},
381+
})
382+
383+
// =============================================================================
384+
// WEIGHTED LEAST CONNECTIONS
385+
// =============================================================================
386+
console.log('6️⃣🔢 Weighted Least Connections Strategy')
387+
388+
gateway.addRoute({
389+
pattern: '/api/wlc/*',
390+
loadBalancer: {
391+
strategy: 'weighted-least-connections',
392+
targets: [
393+
{ url: 'http://localhost:8080', weight: 3 },
394+
{ url: 'http://localhost:8081', weight: 1 },
395+
],
396+
healthCheck: {
397+
enabled: true,
398+
interval: 12000,
399+
timeout: 4000,
400+
path: '/',
401+
expectedStatus: 200,
402+
},
403+
},
404+
proxy: {
405+
pathRewrite: (path) => path.replace('/api/wlc', ''),
406+
},
407+
})
408+
409+
// =============================================================================
410+
// MONITORING AND METRICS ENDPOINT
300411
// =============================================================================
301412
console.log('7️⃣ Monitoring and Metrics')
302413

@@ -327,7 +438,7 @@ gateway.addRoute({
327438
})
328439

329440
// =============================================================================
330-
// 8. HEALTH CHECK ENDPOINT
441+
// HEALTH CHECK ENDPOINT
331442
// =============================================================================
332443
gateway.addRoute({
333444
pattern: '/health',
@@ -348,7 +459,7 @@ gateway.addRoute({
348459
})
349460

350461
// =============================================================================
351-
// 9. DEMO ENDPOINTS FOR TESTING
462+
// DEMO ENDPOINTS FOR TESTING
352463
// =============================================================================
353464
gateway.addRoute({
354465
pattern: '/demo',
@@ -359,6 +470,10 @@ gateway.addRoute({
359470
'Least Connections': '/api/least-connections/get',
360471
'IP Hash': '/api/ip-hash/get',
361472
Random: '/api/random/get',
473+
'Power of Two Choices (P2C)': '/api/p2c/get',
474+
'P2C Alias (power-of-two-choices)': '/api/power-of-two/get',
475+
'Latency-based': '/api/latency/get',
476+
'Weighted Least Connections': '/api/wlc/get',
362477
'Users Service': '/api/users/1',
363478
'Posts Service': '/api/posts/1',
364479
Metrics: '/metrics',
@@ -430,6 +545,10 @@ try {
430545
console.log(' • Least Connections: /api/least-connections/*')
431546
console.log(' • IP Hash: /api/ip-hash/*')
432547
console.log(' • Random: /api/random/*')
548+
console.log(' • Power of Two Choices (P2C): /api/p2c/*')
549+
console.log(' • P2C Alias (power-of-two-choices): /api/power-of-two/*')
550+
console.log(' • Latency-based: /api/latency/*')
551+
console.log(' • Weighted Least Connections: /api/wlc/*')
433552
console.log(' • Users Service: /api/users/*')
434553
console.log(' • Posts Service: /api/posts/*')
435554

src/gateway/gateway.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,8 @@ export class BunGateway implements Gateway {
329329
}
330330
}
331331

332+
// Measure end-to-end time to update latency metrics in the load balancer
333+
const startedAt = Date.now()
332334
increaseTargetConnectionsIfLeastConnections(
333335
route.loadBalancer?.strategy,
334336
target,
@@ -347,12 +349,22 @@ export class BunGateway implements Gateway {
347349
route.loadBalancer?.strategy,
348350
target,
349351
)
352+
// Update latency stats for strategies like 'latency' and as tie-breakers
353+
try {
354+
const duration = Date.now() - startedAt
355+
loadBalancer.recordResponse(target.url, duration, false)
356+
} catch {}
350357
},
351358
onError: (req: Request, error: Error) => {
352359
decreaseTargetConnectionsIfLeastConnections(
353360
route.loadBalancer?.strategy,
354361
target,
355362
)
363+
// Record error with latency to penalize target appropriately
364+
try {
365+
const duration = Date.now() - startedAt
366+
loadBalancer.recordResponse(target.url, duration, true)
367+
} catch {}
356368
if (route.hooks?.onError) {
357369
route.hooks.onError!(req, error)
358370
}
@@ -490,7 +502,13 @@ function increaseTargetConnectionsIfLeastConnections(
490502
strategy: string | undefined,
491503
target: any,
492504
): void {
493-
if (strategy === 'least-connections' && target.connections !== undefined) {
505+
if (
506+
(strategy === 'least-connections' ||
507+
strategy === 'weighted-least-connections' ||
508+
strategy === 'p2c' ||
509+
strategy === 'power-of-two-choices') &&
510+
target.connections !== undefined
511+
) {
494512
target.connections++
495513
}
496514
}
@@ -499,7 +517,13 @@ function decreaseTargetConnectionsIfLeastConnections(
499517
strategy: string | undefined,
500518
target: any,
501519
): void {
502-
if (strategy === 'least-connections' && target.connections !== undefined) {
520+
if (
521+
(strategy === 'least-connections' ||
522+
strategy === 'weighted-least-connections' ||
523+
strategy === 'p2c' ||
524+
strategy === 'power-of-two-choices') &&
525+
target.connections !== undefined
526+
) {
503527
target.connections--
504528
}
505529
}

src/interfaces/load-balancer.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ export interface LoadBalancerConfig {
6969
| 'random' // Randomly selects a target
7070
| 'weighted' // Uses target weights for distribution
7171
| 'ip-hash' // Routes based on client IP hash for session affinity
72+
| 'p2c' // Power of two choices: pick best of two random targets
73+
| 'power-of-two-choices' // Alias for p2c
74+
| 'latency' // Chooses target with the lowest avg response time
75+
| 'weighted-least-connections' // Least connections normalized by weight
7276

7377
/**
7478
* List of backend targets to load balance across
@@ -113,6 +117,11 @@ export interface LoadBalancerConfig {
113117
* @example 'OK' or 'healthy'
114118
*/
115119
expectedBody?: string
120+
/**
121+
* HTTP method to use for health checks
122+
* @default 'GET'
123+
*/
124+
method?: 'GET' | 'HEAD'
116125
}
117126

118127
/**

0 commit comments

Comments
 (0)