A minimal, generic Golang library for implementing smooth weighted round-robin (SWRR) load balancing with dynamic self-healing, administrative controls, and Nginx-style effective weight degradation.
go get github.com/mr-karan/balance- Go Generics: Fully type-safe balancing for any
comparabletype (e.g., strings, connection pools, goroutine channels, or custom structs). - Nginx Effective Weight Algorithm: Automatic degradation of selection priority on node failures (
RecordFailure) and gradual recovery on success (RecordSuccess). - Dynamic Adjustments: Update node weights (
UpdateWeight) and toggle administrative active/inactive states (SetActive) on the fly. - Self-Healing Fallback: Prevents total starvation by automatically resetting effective weights if all active nodes degrade to
0.
package main
import (
"fmt"
"github.com/mr-karan/balance"
)
func main() {
// Create a new generic load balancer for string IDs
b := balance.NewBalance[string]()
// Add items with their corresponding weights
b.Add("server-a", 5)
b.Add("server-b", 3)
b.Add("server-c", 2)
// Get the next item (sequence will be smoothly balanced: a b c a a b a c b a)
for i := 0; i < 10; i++ {
fmt.Println(b.Get())
}
}When targets fail, report errors to decrease their likelihood of selection. Once healthy again, report success to restore their original weight.
// Record a failure: lowers the priority of server-a
b.RecordFailure("server-a")
// Balance continues, but server-a gets selected less frequently
target := b.Get()
// Record a success: restores the priority of server-a back to its base weight
b.RecordSuccess("server-a")// Dynamically adjust weight to 10
b.UpdateWeight("server-a", 10)
// Administratively deactivate server-b (will be skipped by Get() until reactivated)
b.SetActive("server-b", false)The algorithm is based on the Smooth Weighted Round Robin used by NGINX.
On each peer selection we increase the current_weight of each eligible peer by its effective_weight, select the peer with the greatest current_weight, and reduce its current_weight by the total number of weight points distributed among peers.
For edge case weights like { 5, 1, 1 }, this algorithm produces the smooth sequence { a, a, b, a, c, a, a } instead of the unbalanced sequence { c, b, a, a, a, a, a } produced by basic round robin.
This shows the sequence of current_weight values after each selection:
a b c
0 0 0 (initial state)
5 1 1 (a selected)
-2 1 1
3 2 2 (a selected)
-4 2 2
1 3 3 (b selected)
1 -4 3
6 -3 4 (a selected)
-1 -3 4
4 -2 5 (c selected)
4 -2 -2
9 -1 -1 (a selected)
2 -1 -1
7 0 0 (a selected)
0 0 0
To preserve weight reduction in case of failures, the effective_weight variable is used. It usually matches the peer's configured weight, but is reduced temporarily on peer failures. This avoids loops with backup servers and prevents skipping alive upstreams when multiple dead ones exist.
To see how this load balancer can be used in a real-world, concurrent Go worker pool (complete with circuit-breakers, metric-based dynamic weighting, and auto-recovery), run our simulation suite directly from the root:
go test -v -run TestWorkerPoolSimulationThis simulation runs concurrent tasks across workers with varying speeds and fault tolerances, demonstrating SWRR task routing, error-based eviction, and automatic worker recovery.
Run the benchmarks locally:
go test -v -failfast -bench=. -benchmem -run=^$Example output:
goos: linux
goarch: amd64
pkg: github.com/mr-karan/balance
cpu: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz
BenchmarkBalance
BenchmarkBalance/items-10
BenchmarkBalance/items-10-8 18249529 63.82 ns/op 0 B/op 0 allocs/op
BenchmarkBalance/items-100
BenchmarkBalance/items-100-8 9840943 119.5 ns/op 0 B/op 0 allocs/op
BenchmarkBalance/items-1000
BenchmarkBalance/items-1000-8 1608460 767.1 ns/op 0 B/op 0 allocs/op
BenchmarkBalance/items-10000
BenchmarkBalance/items-10000-8 123394 9621 ns/op 0 B/op 0 allocs/op
BenchmarkBalance/items-100000
BenchmarkBalance/items-100000-8 10000 102295 ns/op 0 B/op 0 allocs/op
PASS
ok github.com/mr-karan/balance 7.927s