From 80611f3830e91862732019d9f8a17d074c8ac788 Mon Sep 17 00:00:00 2001 From: Andriy Yevtushyn <129842532+Avionic23@users.noreply.github.com> Date: Wed, 27 May 2026 17:13:25 +0300 Subject: [PATCH] feat: add YAML config loader for server and backend settings test: add config loader tests feat: wire config into main for port, algorithm, and backends refactor: replace NewBackend, NewProxy args with BackendOptions and ProxyOptions struct --- backend/backend.go | 12 ++++++-- config/config_test.yaml | 9 ++++++ config/loader.go | 50 ++++++++++++++++++++++++++++++++ config/loader_test.go | 11 +++++++ go.mod | 2 ++ go.sum | 2 ++ main.go | 39 ++++++++++++++++++------- proxy/proxy.go | 11 +++++-- router/roundrobin/round_robin.go | 6 ++-- 9 files changed, 123 insertions(+), 19 deletions(-) create mode 100644 config/config_test.yaml create mode 100644 config/loader.go create mode 100644 config/loader_test.go create mode 100644 go.sum diff --git a/backend/backend.go b/backend/backend.go index 4c62b9e..c5a36d4 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -10,8 +10,12 @@ type Backend struct { healthy atomic.Bool } -func NewBackend(url string) *Backend { - return &Backend{url: url} +type BackendOptions struct { + Url string +} + +func NewBackend(opts BackendOptions) *Backend { + return &Backend{url: opts.Url} } func (b *Backend) GetUrl() string { @@ -22,6 +26,10 @@ func (b *Backend) IsHealthy() bool { return b.healthy.Load() } +func (b *Backend) SetHealth(isHealthy bool) { + b.healthy.Store(isHealthy) +} + type BackendPool struct { bs []*Backend mu sync.RWMutex diff --git a/config/config_test.yaml b/config/config_test.yaml new file mode 100644 index 0000000..eebbbec --- /dev/null +++ b/config/config_test.yaml @@ -0,0 +1,9 @@ +port: 8080 +backends: + - {url: "localhost:80", weight: 10} + - {url: "localhost:8081", weight: 10} +algorithm: "round-robin" +timeouts: + dial: 10s + connect: 20s + shutdown: 30s \ No newline at end of file diff --git a/config/loader.go b/config/loader.go new file mode 100644 index 0000000..781e45a --- /dev/null +++ b/config/loader.go @@ -0,0 +1,50 @@ +package config + +import ( + "fmt" + "go.yaml.in/yaml/v4" + "os" + "time" +) + +type BackendCfg struct { + Url string `yaml:"url"` + Weight int `yaml:"weight"` +} + +type TimeoutsCfg struct { + Dial time.Duration `yaml:"dial"` + Connect time.Duration `yaml:"connect"` + Shutdown time.Duration `yaml:"shutdown"` +} + +type Config struct { + Port int `yaml:"port"` + Algorithm string `yaml:"algorithm"` + Backends []BackendCfg `yaml:"backends"` + Timeouts TimeoutsCfg `yaml:"timeouts"` +} + +func Load(filePath string) Config { + var conf Config + data, err := os.ReadFile(filePath) + if err != nil { + panic(err) + } + if err = yaml.Unmarshal(data, &conf); err != nil { + panic(err) + } + if err = conf.validate(); err != nil { + panic(err) + } + return conf +} + +func (c *Config) validate() error { + for i, b := range c.Backends { + if b.Url == "" { + return fmt.Errorf("backend %d: url required", i) + } + } + return nil +} diff --git a/config/loader_test.go b/config/loader_test.go new file mode 100644 index 0000000..90dea1f --- /dev/null +++ b/config/loader_test.go @@ -0,0 +1,11 @@ +package config + +import ( + "fmt" + "testing" +) + +func TestLoad(t *testing.T) { + config := Load("config_test.yaml") + fmt.Println(config) +} diff --git a/go.mod b/go.mod index 94a0461..e972199 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module load-balancer go 1.24.0 + +require go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..161a01b --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= +go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= diff --git a/main.go b/main.go index 4114e32..427e289 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "load-balancer/backend" + "load-balancer/config" "load-balancer/listener" "load-balancer/proxy" "load-balancer/router" @@ -13,22 +14,38 @@ import ( "os" "os/signal" "syscall" - "time" ) func main() { - port := 8080 + conf := config.Load("config/config_test.yaml") + host := "[::1]" - ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + ln, err := net.Listen("tcp", fmt.Sprintf(":%d", conf.Port)) if err != nil { - log.Fatalf("failed to listen on port %d: %v", port, err) + log.Fatalf("failed to listen on port %d: %v", conf.Port, err) + } + + backends := make([]*backend.Backend, 0, len(conf.Backends)) + for i, b := range conf.Backends { + backends = append(backends, backend.NewBackend(backend.BackendOptions{ + Url: b.Url})) + backends[i].SetHealth(true) } - b := backend.NewBackend("localhost:80") - b1 := backend.NewBackend("localhost:8081") - bp := backend.NewBackendPool([]*backend.Backend{b, b1}) - algo := roundrobin.NewRoundRobin(bp) - r := router.NewRouter(host+fmt.Sprintf(":%d", port), algo) - p := proxy.NewProxy(r) + bp := backend.NewBackendPool(backends) + + var algo router.AlgoIO + switch conf.Algorithm { + case "round-robin": + algo = roundrobin.NewRoundRobin(bp) + default: + panic("unknown algorithm") + } + + r := router.NewRouter(host+fmt.Sprintf(":%d", conf.Port), algo) + p := proxy.NewProxy(r, proxy.ProxyOptions{ + DialTimeout: conf.Timeouts.Dial, + ConnTimeout: conf.Timeouts.Connect, + }) l := listener.NewListener(p) go l.Listen(ln) @@ -37,7 +54,7 @@ func main() { signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT) <-sigCh - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), conf.Timeouts.Shutdown) defer cancel() l.GracefulShutdown(ctx) diff --git a/proxy/proxy.go b/proxy/proxy.go index 2ded4af..cf90d3f 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -18,14 +18,19 @@ type Proxy struct { connTimeout time.Duration } -func NewProxy(rt RouterIO) *Proxy { +type ProxyOptions struct { + DialTimeout time.Duration + ConnTimeout time.Duration +} + +func NewProxy(rt RouterIO, opts ProxyOptions) *Proxy { if rt == nil { panic("router cannot be nil") } return &Proxy{ router: rt, - dialTimeout: 10 * time.Second, - connTimeout: 20 * time.Second, + dialTimeout: opts.DialTimeout, + connTimeout: opts.ConnTimeout, } } diff --git a/router/roundrobin/round_robin.go b/router/roundrobin/round_robin.go index f526e94..581f32c 100644 --- a/router/roundrobin/round_robin.go +++ b/router/roundrobin/round_robin.go @@ -18,11 +18,11 @@ type RoundRobin struct { mu sync.Mutex } -func NewRoundRobin(bs BackendPoolIO) *RoundRobin { - if bs == nil { +func NewRoundRobin(bp BackendPoolIO) *RoundRobin { + if bp == nil { panic("backend pool cannot be nil") } - return &RoundRobin{bp: bs} + return &RoundRobin{bp: bp} } func (rr *RoundRobin) GetBackend() (*backend.Backend, error) {