@@ -151,6 +151,18 @@ export class HttpLoadBalancer implements LoadBalancer {
151151 return null
152152 }
153153
154+ // Fast path: only one healthy target
155+ if ( healthyTargets . length === 1 ) {
156+ const only = healthyTargets [ 0 ] !
157+ this . recordRequest ( only . url )
158+ this . logger . logLoadBalancing ( this . config . strategy , only . url , {
159+ reason : 'single-healthy' ,
160+ duration : Date . now ( ) - startTime ,
161+ healthyTargets : 1 ,
162+ } )
163+ return only
164+ }
165+
154166 // Check for sticky session first
155167 if ( this . config . stickySession ?. enabled ) {
156168 const stickyTarget = this . getStickyTarget ( request )
@@ -184,6 +196,16 @@ export class HttpLoadBalancer implements LoadBalancer {
184196 case 'ip-hash' :
185197 selectedTarget = this . selectIpHash ( request , healthyTargets )
186198 break
199+ case 'p2c' :
200+ case 'power-of-two-choices' :
201+ selectedTarget = this . selectPowerOfTwoChoices ( healthyTargets )
202+ break
203+ case 'latency' :
204+ selectedTarget = this . selectByLatency ( healthyTargets )
205+ break
206+ case 'weighted-least-connections' :
207+ selectedTarget = this . selectWeightedLeastConnections ( healthyTargets )
208+ break
187209 default :
188210 selectedTarget = this . selectRoundRobin ( healthyTargets )
189211 }
@@ -378,7 +400,8 @@ export class HttpLoadBalancer implements LoadBalancer {
378400 const target = this . targets . get ( url )
379401 if ( target ) {
380402 target . totalResponseTime += responseTime
381- target . averageResponseTime = target . totalResponseTime / target . requests
403+ const reqs = target . requests || 1 // avoid division by zero before first selectTarget
404+ target . averageResponseTime = target . totalResponseTime / reqs
382405
383406 if ( isError ) {
384407 target . errors ++
@@ -478,6 +501,69 @@ export class HttpLoadBalancer implements LoadBalancer {
478501 return targets [ index ] ! // Guaranteed to exist due to length check
479502 }
480503
504+ private selectPowerOfTwoChoices (
505+ targets : LoadBalancerTarget [ ] ,
506+ ) : LoadBalancerTarget {
507+ // Sample two distinct targets at random and choose the better one by connections then latency
508+ const i = Math . floor ( Math . random ( ) * targets . length )
509+ let j = Math . floor ( Math . random ( ) * targets . length )
510+ if ( j === i ) j = ( j + 1 ) % targets . length
511+ const a = targets [ i ] !
512+ const b = targets [ j ] !
513+ return this . betterByLoadThenLatency ( a , b )
514+ }
515+
516+ private selectByLatency ( targets : LoadBalancerTarget [ ] ) : LoadBalancerTarget {
517+ // Choose the target with the smallest averageResponseTime; fallback to round-robin if missing data
518+ let best = targets [ 0 ] !
519+ let bestLatency = best . averageResponseTime ?? Number . POSITIVE_INFINITY
520+ for ( let k = 1 ; k < targets . length ; k ++ ) {
521+ const t = targets [ k ] !
522+ const lat = t . averageResponseTime ?? Number . POSITIVE_INFINITY
523+ if ( lat < bestLatency ) {
524+ best = t
525+ bestLatency = lat
526+ }
527+ }
528+ if ( ! isFinite ( bestLatency ) ) {
529+ // No latency data available yet
530+ return this . selectRoundRobin ( targets )
531+ }
532+ return best
533+ }
534+
535+ private selectWeightedLeastConnections (
536+ targets : LoadBalancerTarget [ ] ,
537+ ) : LoadBalancerTarget {
538+ // Choose by minimal (connections + 1) / weight
539+ return targets . reduce ( ( best , curr ) => {
540+ const bestScore =
541+ ( Math . max ( 0 , best . connections ?? 0 ) + 1 ) / Math . max ( 1 , best . weight ?? 1 )
542+ const currScore =
543+ ( Math . max ( 0 , curr . connections ?? 0 ) + 1 ) / Math . max ( 1 , curr . weight ?? 1 )
544+ if ( currScore < bestScore ) return curr
545+ if ( currScore === bestScore ) return this . betterByLatency ( best , curr )
546+ return best
547+ } )
548+ }
549+
550+ private betterByLatency ( a : LoadBalancerTarget , b : LoadBalancerTarget ) {
551+ const la = a . averageResponseTime ?? Number . POSITIVE_INFINITY
552+ const lb = b . averageResponseTime ?? Number . POSITIVE_INFINITY
553+ return la <= lb ? a : b
554+ }
555+
556+ private betterByLoadThenLatency (
557+ a : LoadBalancerTarget ,
558+ b : LoadBalancerTarget ,
559+ ) {
560+ const ca = a . connections ?? 0
561+ const cb = b . connections ?? 0
562+ if ( ca < cb ) return a
563+ if ( cb < ca ) return b
564+ return this . betterByLatency ( a , b )
565+ }
566+
481567 private recordRequest ( url : string ) : void {
482568 this . totalRequests ++
483569 const target = this . targets . get ( url )
@@ -552,10 +638,21 @@ export class HttpLoadBalancer implements LoadBalancer {
552638 }
553639
554640 private getClientId ( request : Request ) : string {
555- // In real scenario, would extract actual client IP
556- // For now, use a combination of headers as identifier
557- const userAgent = request . headers . get ( 'user-agent' ) ?? ''
558- const accept = request . headers . get ( 'accept' ) ?? ''
641+ // Prefer real client IP headers commonly set by proxies/CDNs; fallback to UA+Accept
642+ const headers = request . headers
643+ const xff = headers . get ( 'x-forwarded-for' ) || headers . get ( 'X-Forwarded-For' )
644+ if ( xff ) {
645+ const ip = xff . split ( ',' ) [ 0 ] ! . trim ( )
646+ if ( ip ) return ip
647+ }
648+ const realIp =
649+ headers . get ( 'x-real-ip' ) ||
650+ headers . get ( 'cf-connecting-ip' ) ||
651+ headers . get ( 'x-client-ip' ) ||
652+ ''
653+ if ( realIp ) return realIp
654+ const userAgent = headers . get ( 'user-agent' ) ?? ''
655+ const accept = headers . get ( 'accept' ) ?? ''
559656 return userAgent + accept
560657 }
561658
@@ -596,7 +693,7 @@ export class HttpLoadBalancer implements LoadBalancer {
596693
597694 const response = await fetch ( url . toString ( ) , {
598695 signal : controller . signal ,
599- method : 'GET' ,
696+ method : healthCheckConfig . method ?? 'GET' ,
600697 } )
601698
602699 clearTimeout ( timeoutId )
0 commit comments